diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..0385e3430 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true diff --git a/README.md b/README.md index 3f9fdb9e4..f694825a7 100644 --- a/README.md +++ b/README.md @@ -176,3 +176,10 @@ Development instructions for the CircleCI CLI can be found in [HACKING.md](HACKI ## More Please see the [documentation](https://circleci-public.github.io/circleci-cli) or `circleci help` for more. + + +## Version Compatibility + +As of version `0.1.24705` - we no longer support Server 3.x instances. In order to upgrade the CLI to the latest version, you'll need to update your instance of server to 4.x. + +`0.1.23845` is the last version to support Server 3.x and 2.x. diff --git a/api/api.go b/api/api.go index ff6ca667d..3ea130e0d 100644 --- a/api/api.go +++ b/api/api.go @@ -3,7 +3,7 @@ package api import ( "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/http" "os" @@ -484,9 +484,9 @@ func loadYaml(path string) (string, error) { var err error var config []byte if path == "-" { - config, err = ioutil.ReadAll(os.Stdin) + config, err = io.ReadAll(os.Stdin) } else { - config, err = ioutil.ReadFile(path) + config, err = os.ReadFile(path) } if err != nil { diff --git a/api/context_rest.go b/api/context_rest.go index d953bc7aa..ce484ea7c 100644 --- a/api/context_rest.go +++ b/api/context_rest.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" @@ -71,7 +70,7 @@ func (c *ContextRestClient) DeleteEnvironmentVariable(contextID, variable string return err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err @@ -99,7 +98,7 @@ func (c *ContextRestClient) CreateContextWithOrgID(orgID *string, name string) e return err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err @@ -131,7 +130,7 @@ func (c *ContextRestClient) CreateContext(vcs, org, name string) error { return err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err @@ -162,7 +161,7 @@ func (c *ContextRestClient) CreateEnvironmentVariable(contextID, variable, value return err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err @@ -190,7 +189,7 @@ func (c *ContextRestClient) DeleteContext(contextID string) error { return err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err @@ -300,7 +299,7 @@ func (c *ContextRestClient) listEnvironmentVariables(params *listEnvironmentVari return nil, err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err @@ -334,7 +333,7 @@ func (c *ContextRestClient) listContexts(params *listContextsParams) (*listConte return nil, err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err @@ -580,7 +579,7 @@ func (c *ContextRestClient) EnsureExists() error { return errors.New("API v2 test request failed.") } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err diff --git a/api/context_rest_test.go b/api/context_rest_test.go index 1c969721d..005c64908 100644 --- a/api/context_rest_test.go +++ b/api/context_rest_test.go @@ -2,7 +2,7 @@ package api import ( "fmt" - "io/ioutil" + "io" "net/http" "github.com/CircleCI-Public/circleci-cli/settings" @@ -32,7 +32,7 @@ func appendRESTPostHandler(server *ghttp.Server, combineHandlers ...MockRequestR ghttp.VerifyRequest("POST", "/api/v2/context"), ghttp.VerifyContentType("application/json"), func(w http.ResponseWriter, req *http.Request) { - body, err := ioutil.ReadAll(req.Body) + body, err := io.ReadAll(req.Body) Expect(err).ShouldNot(HaveOccurred()) err = req.Body.Close() Expect(err).ShouldNot(HaveOccurred()) diff --git a/api/graphql/client.go b/api/graphql/client.go index 9392324f9..d5c5d8613 100644 --- a/api/graphql/client.go +++ b/api/graphql/client.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/http" "net/url" @@ -254,7 +254,7 @@ func (cl *Client) Run(request *Request, resp interface{}) error { if cl.Debug { var bodyBytes []byte if res.Body != nil { - bodyBytes, err = ioutil.ReadAll(res.Body) + bodyBytes, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "reading response") } @@ -262,7 +262,7 @@ func (cl *Client) Run(request *Request, resp interface{}) error { l.Printf("<< %s", string(bodyBytes)) // Restore the io.ReadCloser to its original state - res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) + res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } } diff --git a/api/graphql/client_test.go b/api/graphql/client_test.go index b0a27d471..2edf3ef9c 100644 --- a/api/graphql/client_test.go +++ b/api/graphql/client_test.go @@ -2,7 +2,6 @@ package graphql import ( "io" - "io/ioutil" "net/http" "net/http/httptest" "regexp" @@ -53,7 +52,7 @@ func TestDoJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ - b, err := ioutil.ReadAll(r.Body) + b, err := io.ReadAll(r.Body) if err != nil { t.Errorf(err.Error()) } @@ -96,7 +95,7 @@ func TestQueryJSON(t *testing.T) { var calls int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ - b, err := ioutil.ReadAll(r.Body) + b, err := io.ReadAll(r.Body) if err != nil { t.Errorf(err.Error()) } @@ -146,7 +145,7 @@ func TestDoJSONErr(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { calls++ - body, err := ioutil.ReadAll(req.Body) + body, err := io.ReadAll(req.Body) if err != nil { t.Errorf(err.Error()) } diff --git a/api/info/info.go b/api/info/info.go index cb7fae40b..df1c79455 100644 --- a/api/info/info.go +++ b/api/info/info.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/url" @@ -58,7 +57,7 @@ func (c *InfoRESTClient) GetInfo() (*[]Organization, error) { return nil, err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err diff --git a/api/rest/client.go b/api/rest/client.go index 57c7bfcc5..219363301 100644 --- a/api/rest/client.go +++ b/api/rest/client.go @@ -17,14 +17,14 @@ import ( ) type Client struct { - baseURL *url.URL + BaseURL *url.URL circleToken string client *http.Client } func New(baseURL *url.URL, token string, httpClient *http.Client) *Client { return &Client{ - baseURL: baseURL, + BaseURL: baseURL, circleToken: token, client: httpClient, } @@ -60,7 +60,7 @@ func (c *Client) NewRequest(method string, u *url.URL, payload interface{}) (req } } - req, err = http.NewRequest(method, c.baseURL.ResolveReference(u).String(), r) + req, err = http.NewRequest(method, c.BaseURL.ResolveReference(u).String(), r) if err != nil { return nil, err } diff --git a/api/schedule_rest.go b/api/schedule_rest.go index 6b35d700f..c422b66a0 100644 --- a/api/schedule_rest.go +++ b/api/schedule_rest.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" @@ -49,7 +48,7 @@ func (c *ScheduleRestClient) CreateSchedule(vcs, org, project, name, description return nil, err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err @@ -86,7 +85,7 @@ func (c *ScheduleRestClient) UpdateSchedule(scheduleID, name, description string return nil, err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err @@ -121,7 +120,7 @@ func (c *ScheduleRestClient) DeleteSchedule(scheduleID string) error { return err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err @@ -157,7 +156,7 @@ func (c *ScheduleRestClient) ScheduleByID(scheduleID string) (*Schedule, error) return nil, err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err @@ -231,7 +230,7 @@ func (c *ScheduleRestClient) listSchedules(vcs, org, project string, params *lis return nil, err } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err @@ -375,7 +374,7 @@ func (c *ScheduleRestClient) newDeleteScheduleRequest(scheduleID string) (*http. return c.newHTTPRequest("DELETE", queryURL.String(), nil) } -// Builds a requeest to list schedules according to params. +// Builds a request to list schedules according to params. func (c *ScheduleRestClient) newListSchedulesRequest(vcs, org, project string, params *listSchedulesParams) (*http.Request, error) { var err error queryURL, err := url.Parse(c.server) @@ -438,7 +437,7 @@ func (c *ScheduleRestClient) EnsureExists() error { return errors.New("API v2 test request failed.") } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return err diff --git a/clitest/clitest.go b/clitest/clitest.go index e6ae4de59..6a0010d5c 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -5,7 +5,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "net/http" "os" "path/filepath" @@ -50,7 +49,7 @@ func (tempSettings TempSettings) AssertConfigRereadMatches(contents string) { file, err := os.Open(tempSettings.Config.Path) gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - reread, err := ioutil.ReadAll(file) + reread, err := io.ReadAll(file) gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) gomega.Expect(string(reread)).To(gomega.ContainSubstring(contents)) } @@ -61,7 +60,7 @@ func WithTempSettings() *TempSettings { tempSettings := &TempSettings{} - tempSettings.Home, err = ioutil.TempDir("", "circleci-cli-test-") + tempSettings.Home, err = os.MkdirTemp("", "circleci-cli-test-") gomega.Expect(err).ToNot(gomega.HaveOccurred()) settingsPath := filepath.Join(tempSettings.Home, ".circleci") @@ -101,7 +100,7 @@ func (tempSettings *TempSettings) AppendRESTPostHandler(combineHandlers ...MockR ghttp.VerifyRequest("POST", "/api/v2/context"), ghttp.VerifyContentType("application/json"), func(w http.ResponseWriter, req *http.Request) { - body, err := ioutil.ReadAll(req.Body) + body, err := io.ReadAll(req.Body) gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) err = req.Body.Close() gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) @@ -131,7 +130,7 @@ func (tempSettings *TempSettings) AppendPostHandler(authToken string, combineHan // VerifyContentType("application/json") check // that fails with "application/json; charset=utf-8" func(w http.ResponseWriter, req *http.Request) { - body, err := ioutil.ReadAll(req.Body) + body, err := io.ReadAll(req.Body) gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) err = req.Body.Close() gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) @@ -152,7 +151,7 @@ func (tempSettings *TempSettings) AppendPostHandler(authToken string, combineHan // VerifyContentType("application/json") check // that fails with "application/json; charset=utf-8" func(w http.ResponseWriter, req *http.Request) { - body, err := ioutil.ReadAll(req.Body) + body, err := io.ReadAll(req.Body) gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) err = req.Body.Close() gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) diff --git a/cmd/admin_test.go b/cmd/admin_test.go index 21baab91e..40f330e63 100644 --- a/cmd/admin_test.go +++ b/cmd/admin_test.go @@ -251,7 +251,7 @@ var _ = Describe("Namespace integration tests", func() { } }` - expectedDeleteNamespacerequest := `{ + expectedDeleteNamespaceRequest := `{ "query": "\nmutation($id: UUID!) {\n deleteNamespaceAndRelatedOrbs(namespaceId: $id) {\n deleted\n errors {\n type\n message\n }\n }\n}\n", "variables": { "id": "f13a9e13-538c-435c-8f61-78596661acd6" @@ -270,7 +270,7 @@ var _ = Describe("Namespace integration tests", func() { tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ Status: http.StatusOK, - Request: expectedDeleteNamespacerequest, + Request: expectedDeleteNamespaceRequest, Response: gqlDeleteNamespaceResponse}) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -324,7 +324,7 @@ var _ = Describe("Namespace integration tests", func() { } }` - expectedDeleteNamespacerequest := `{ + expectedDeleteNamespaceRequest := `{ "query": "\nmutation($id: UUID!) {\n deleteNamespaceAndRelatedOrbs(namespaceId: $id) {\n deleted\n errors {\n type\n message\n }\n }\n}\n", "variables": { "id": "f13a9e13-538c-435c-8f61-78596661acd6" @@ -343,7 +343,7 @@ var _ = Describe("Namespace integration tests", func() { tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ Status: http.StatusOK, - Request: expectedDeleteNamespacerequest, + Request: expectedDeleteNamespaceRequest, Response: gqlDeleteNamespaceResponse}) session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) diff --git a/cmd/config.go b/cmd/config.go index a30c82d78..d2afbef52 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -2,55 +2,27 @@ package cmd import ( "fmt" - "io/ioutil" - "net/url" - "strings" - "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/config" "github.com/CircleCI-Public/circleci-cli/filetree" - "github.com/CircleCI-Public/circleci-cli/local" - "github.com/CircleCI-Public/circleci-cli/pipeline" "github.com/CircleCI-Public/circleci-cli/proxy" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/spf13/pflag" "gopkg.in/yaml.v3" ) -var ( - CollaborationsPath = "me/collaborations" -) - -type configOptions struct { - cfg *settings.Config - rest *rest.Client - args []string -} - // Path to the config.yml file to operate on. // Used to for compatibility with `circleci config validate --path` var configPath string var ignoreDeprecatedImages bool // should we ignore deprecated images warning +var verboseOutput bool // Enable extra debugging output var configAnnotations = map[string]string{ "": "The path to your config (use \"-\" for STDIN)", } -func GetConfigAPIHost(cfg *settings.Config) string { - if cfg.Host != defaultHost { - return cfg.Host - } else { - return cfg.ConfigAPIHost - } -} - -func newConfigCommand(config *settings.Config) *cobra.Command { - opts := configOptions{ - cfg: config, - } - +func newConfigCommand(globalConfig *settings.Config) *cobra.Command { configCmd := &cobra.Command{ Use: "config", Short: "Operate on build config files", @@ -59,11 +31,8 @@ func newConfigCommand(config *settings.Config) *cobra.Command { packCommand := &cobra.Command{ Use: "pack ", Short: "Pack up your CircleCI configuration into a single file.", - PreRun: func(cmd *cobra.Command, args []string) { - opts.args = args - }, - RunE: func(_ *cobra.Command, _ []string) error { - return packConfig(opts) + RunE: func(_ *cobra.Command, args []string) error { + return packConfig(args) }, Args: cobra.ExactArgs(1), Annotations: make(map[string]string), @@ -74,19 +43,33 @@ func newConfigCommand(config *settings.Config) *cobra.Command { Use: "validate ", Aliases: []string{"check"}, Short: "Check that the config file is well formed.", - PreRun: func(cmd *cobra.Command, args []string) { - opts.args = args - opts.rest = rest.NewFromConfig(GetConfigAPIHost(opts.cfg), config) - }, - RunE: func(cmd *cobra.Command, _ []string) error { - return validateConfig(opts, cmd.Flags()) + RunE: func(cmd *cobra.Command, args []string) error { + compiler := config.New(globalConfig) + orgID, _ := cmd.Flags().GetString("org-id") + orgSlug, _ := cmd.Flags().GetString("org-slug") + path := config.DefaultConfigPath + if configPath != "" { + path = configPath + } + if len(args) == 1 { + path = args[0] + } + return compiler.ValidateConfig(config.ValidateConfigOpts{ + ConfigPath: path, + OrgID: orgID, + OrgSlug: orgSlug, + IgnoreDeprecatedImages: ignoreDeprecatedImages, + VerboseOutput: verboseOutput, + }) }, Args: cobra.MaximumNArgs(1), Annotations: make(map[string]string), } validateCommand.Annotations[""] = configAnnotations[""] validateCommand.PersistentFlags().StringVarP(&configPath, "config", "c", ".circleci/config.yml", "path to config file") + validateCommand.PersistentFlags().BoolVarP(&verboseOutput, "verbose", "v", false, "Enable verbose output") validateCommand.PersistentFlags().BoolVar(&ignoreDeprecatedImages, "ignore-deprecated-images", false, "ignores the deprecated images error") + if err := validateCommand.PersistentFlags().MarkHidden("config"); err != nil { panic(err) } @@ -96,12 +79,25 @@ func newConfigCommand(config *settings.Config) *cobra.Command { processCommand := &cobra.Command{ Use: "process ", Short: "Validate config and display expanded configuration.", - PreRun: func(cmd *cobra.Command, args []string) { - opts.args = args - opts.rest = rest.NewFromConfig(GetConfigAPIHost(opts.cfg), config) - }, - RunE: func(cmd *cobra.Command, _ []string) error { - return processConfig(opts, cmd.Flags()) + RunE: func(cmd *cobra.Command, args []string) error { + compiler := config.New(globalConfig) + pipelineParamsFilePath, _ := cmd.Flags().GetString("pipeline-parameters") + orgID, _ := cmd.Flags().GetString("org-id") + orgSlug, _ := cmd.Flags().GetString("org-slug") + path := config.DefaultConfigPath + if configPath != "" { + path = configPath + } + if len(args) == 1 { + path = args[0] + } + return compiler.ProcessConfig(config.ProcessConfigOpts{ + ConfigPath: path, + OrgID: orgID, + OrgSlug: orgSlug, + PipelineParamsFilePath: pipelineParamsFilePath, + VerboseOutput: verboseOutput, + }) }, Args: cobra.ExactArgs(1), Annotations: make(map[string]string), @@ -110,16 +106,13 @@ func newConfigCommand(config *settings.Config) *cobra.Command { processCommand.Flags().StringP("org-slug", "o", "", "organization slug (for example: github/example-org), used when a config depends on private orbs belonging to that org") processCommand.Flags().String("org-id", "", "organization id used when a config depends on private orbs belonging to that org") processCommand.Flags().StringP("pipeline-parameters", "", "", "YAML/JSON map of pipeline parameters, accepts either YAML/JSON directly or file path (for example: my-params.yml)") - processCommand.Flags().StringP("circleci-api-host", "", "", "the api-host you want to use for config processing and validation") + processCommand.PersistentFlags().BoolVar(&verboseOutput, "verbose", false, "adds verbose output to the command") migrateCommand := &cobra.Command{ Use: "migrate", Short: "Migrate a pre-release 2.0 config to the official release version", - PreRun: func(cmd *cobra.Command, args []string) { - opts.args = args - }, - RunE: func(_ *cobra.Command, _ []string) error { - return migrateConfig(opts) + RunE: func(_ *cobra.Command, args []string) error { + return migrateConfig(args) }, Hidden: true, DisableFlagParsing: true, @@ -136,115 +129,8 @@ func newConfigCommand(config *settings.Config) *cobra.Command { return configCmd } -// The arg is actually optional, in order to support compatibility with the --path flag. -func validateConfig(opts configOptions, flags *pflag.FlagSet) error { - var err error - var response *config.ConfigResponse - path := local.DefaultConfigPath - // First, set the path to configPath set by --path flag for compatibility - if configPath != "" { - path = configPath - } - - // Then, if an arg is passed in, choose that instead - if len(opts.args) == 1 { - path = opts.args[0] - } - - //if no orgId provided use org slug - values := pipeline.LocalPipelineValues() - fmt.Println("Validating config with following values") - printValues(values) - - var orgID string - orgID, _ = flags.GetString("org-id") - if strings.TrimSpace(orgID) != "" { - response, err = config.ConfigQuery(opts.rest, path, orgID, nil, pipeline.LocalPipelineValues()) - if err != nil { - return err - } - } else { - orgSlug, _ := flags.GetString("org-slug") - orgs, err := GetOrgCollaborations(opts.cfg) - if err != nil { - fmt.Println(err.Error()) - } - orgID = GetOrgIdFromSlug(orgSlug, orgs) - response, err = config.ConfigQuery(opts.rest, path, orgID, nil, pipeline.LocalPipelineValues()) - if err != nil { - return err - } - } - - // check if a deprecated Linux VM image is being used - // link here to blog post when available - // returns an error if a deprecated image is used - if !ignoreDeprecatedImages { - err := config.DeprecatedImageCheck(response) - if err != nil { - return err - } - } - - if path == "-" { - fmt.Printf("\nConfig input is valid.\n") - } else { - fmt.Printf("\nConfig file at %s is valid.\n", path) - } - - return nil -} - -func processConfig(opts configOptions, flags *pflag.FlagSet) error { - paramsYaml, _ := flags.GetString("pipeline-parameters") - var response *config.ConfigResponse - var params pipeline.Parameters - var err error - - if len(paramsYaml) > 0 { - // The 'src' value can be a filepath, or a yaml string. If the file cannot be read successfully, - // proceed with the assumption that the value is already valid yaml. - raw, err := ioutil.ReadFile(paramsYaml) - if err != nil { - raw = []byte(paramsYaml) - } - - err = yaml.Unmarshal(raw, ¶ms) - if err != nil { - return fmt.Errorf("invalid 'pipeline-parameters' provided: %s", err.Error()) - } - } - - //if no orgId provided use org slug - values := pipeline.LocalPipelineValues() - fmt.Println("Processing config with following values") - printValues(values) - - orgID, _ := flags.GetString("org-id") - if strings.TrimSpace(orgID) != "" { - response, err = config.ConfigQuery(opts.rest, opts.args[0], orgID, params, values) - if err != nil { - return err - } - } else { - orgSlug, _ := flags.GetString("org-slug") - orgs, err := GetOrgCollaborations(opts.cfg) - if err != nil { - fmt.Println(err.Error()) - } - orgID = GetOrgIdFromSlug(orgSlug, orgs) - response, err = config.ConfigQuery(opts.rest, opts.args[0], orgID, params, values) - if err != nil { - return err - } - } - - fmt.Print(response.OutputYaml) - return nil -} - -func packConfig(opts configOptions) error { - tree, err := filetree.NewTree(opts.args[0]) +func packConfig(args []string) error { + tree, err := filetree.NewTree(args[0]) if err != nil { return errors.Wrap(err, "An error occurred trying to build the tree") } @@ -257,43 +143,6 @@ func packConfig(opts configOptions) error { return nil } -func migrateConfig(opts configOptions) error { - return proxy.Exec([]string{"config", "migrate"}, opts.args) -} - -func printValues(values pipeline.Values) { - for key, value := range values { - fmt.Printf("\t%s:\t%s", key, value) - } -} - -type CollaborationResult struct { - VcsTye string `json:"vcs_type"` - OrgSlug string `json:"slug"` - OrgName string `json:"name"` - OrgId string `json:"id"` - AvatarUrl string `json:"avatar_url"` -} - -// GetOrgCollaborations - fetches all the collaborations for a given user. -func GetOrgCollaborations(cfg *settings.Config) ([]CollaborationResult, error) { - baseClient := rest.NewFromConfig(cfg.Host, cfg) - req, err := baseClient.NewRequest("GET", &url.URL{Path: CollaborationsPath}, nil) - if err != nil { - return nil, err - } - - var resp []CollaborationResult - _, err = baseClient.DoRequest(req, &resp) - return resp, err -} - -// GetOrgIdFromSlug - converts a slug into an orgID. -func GetOrgIdFromSlug(slug string, collaborations []CollaborationResult) string { - for _, v := range collaborations { - if v.OrgSlug == slug { - return v.OrgId - } - } - return "" +func migrateConfig(args []string) error { + return proxy.Exec([]string{"config", "migrate"}, args) } diff --git a/cmd/config_test.go b/cmd/config_test.go index 1d0967e79..d0ed9016d 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -4,15 +4,11 @@ import ( "fmt" "os/exec" "path/filepath" - "testing" "github.com/CircleCI-Public/circleci-cli/clitest" - "github.com/CircleCI-Public/circleci-cli/cmd" - "github.com/CircleCI-Public/circleci-cli/settings" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" - "github.com/stretchr/testify/assert" "gotest.tools/v3/golden" ) @@ -250,17 +246,3 @@ var _ = Describe("Config", func() { }) }) }) - -func TestGetConfigAPIHost(t *testing.T) { - t.Run("tests that we correctly get the config api host when the host is not the default one", func(t *testing.T) { - // if the host isn't equal to `https://circleci.com` then this is likely a server instance and - // wont have the api.X.com subdomain so we should instead just respect the host for config commands - host := cmd.GetConfigAPIHost(&settings.Config{Host: "test"}) - assert.Equal(t, host, "test") - - // If the host passed in is the same as the defaultHost 'https://circleci.com' - then we know this is cloud - // and as such should use the `api.circleci.com` subdomain - host = cmd.GetConfigAPIHost(&settings.Config{Host: "https://circleci.com", ConfigAPIHost: "https://api.circleci.com"}) - assert.Equal(t, host, "https://api.circleci.com") - }) -} diff --git a/cmd/context.go b/cmd/context.go index aebdd6558..bc9ae2d8f 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -3,7 +3,7 @@ package cmd import ( "bufio" "fmt" - "io/ioutil" + "io" "os" "strings" "time" @@ -189,7 +189,7 @@ func showContext(client api.ContextInterface, vcsType, orgName, contextName stri func readSecretValue() (string, error) { stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { - bytes, err := ioutil.ReadAll(os.Stdin) + bytes, err := io.ReadAll(os.Stdin) return string(bytes), err } else { fmt.Print("Enter secret value and press enter: ") diff --git a/cmd/orb.go b/cmd/orb.go index bbd81858e..33a06c855 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "log" "net/http" "os" @@ -1122,7 +1121,7 @@ func initOrb(opts orbOptions) error { return errors.Wrap(err, "Unexpected error") } - body, err := ioutil.ReadAll(req.Body) + body, err := io.ReadAll(req.Body) if err != nil { return errors.Wrap(err, "Unexpected error") } @@ -1385,46 +1384,46 @@ func initOrb(opts orbOptions) error { return y[0] }() - circleConfigSetup, err := ioutil.ReadFile(path.Join(orbPath, ".circleci", "config.yml")) + circleConfigSetup, err := os.ReadFile(path.Join(orbPath, ".circleci", "config.yml")) if err != nil { return err } configSetupString := string(circleConfigSetup) - err = ioutil.WriteFile(path.Join(orbPath, ".circleci", "config.yml"), []byte(orbTemplate(configSetupString, projectName, ownerName, orbName, namespace)), 0644) + err = os.WriteFile(path.Join(orbPath, ".circleci", "config.yml"), []byte(orbTemplate(configSetupString, projectName, ownerName, orbName, namespace)), 0644) if err != nil { return err } - circleConfigDeploy, err := ioutil.ReadFile(path.Join(orbPath, ".circleci", "test-deploy.yml")) + circleConfigDeploy, err := os.ReadFile(path.Join(orbPath, ".circleci", "test-deploy.yml")) if err != nil { return err } configDeployString := string(circleConfigDeploy) - err = ioutil.WriteFile(path.Join(orbPath, ".circleci", "test-deploy.yml"), []byte(orbTemplate(configDeployString, projectName, ownerName, orbName, namespace)), 0644) + err = os.WriteFile(path.Join(orbPath, ".circleci", "test-deploy.yml"), []byte(orbTemplate(configDeployString, projectName, ownerName, orbName, namespace)), 0644) if err != nil { return err } - readme, err := ioutil.ReadFile(path.Join(orbPath, "README.md")) + readme, err := os.ReadFile(path.Join(orbPath, "README.md")) if err != nil { return err } readmeString := string(readme) - err = ioutil.WriteFile(path.Join(orbPath, "README.md"), []byte(orbTemplate(readmeString, projectName, ownerName, orbName, namespace)), 0644) + err = os.WriteFile(path.Join(orbPath, "README.md"), []byte(orbTemplate(readmeString, projectName, ownerName, orbName, namespace)), 0644) if err != nil { return err } - orbRoot, err := ioutil.ReadFile(path.Join(orbPath, "src", "@orb.yml")) + orbRoot, err := os.ReadFile(path.Join(orbPath, "src", "@orb.yml")) if err != nil { return err } orbRootString := string(orbRoot) - err = ioutil.WriteFile(path.Join(orbPath, "src", "@orb.yml"), []byte(orbTemplate(orbRootString, projectName, ownerName, orbName, namespace)), 0644) + err = os.WriteFile(path.Join(orbPath, "src", "@orb.yml"), []byte(orbTemplate(orbRootString, projectName, ownerName, orbName, namespace)), 0644) if err != nil { return err } @@ -1504,7 +1503,7 @@ func initOrb(opts orbOptions) error { } tempOrbFile := filepath.Join(tempOrbDir, "orb.yml") - err = ioutil.WriteFile(tempOrbFile, []byte(packedOrb), 0644) + err = os.WriteFile(tempOrbFile, []byte(packedOrb), 0644) if err != nil { return errors.Wrap(err, "Unable to write packed orb") } diff --git a/cmd/orb_import_test.go b/cmd/orb_import_test.go index d5c66d20d..70412be03 100644 --- a/cmd/orb_import_test.go +++ b/cmd/orb_import_test.go @@ -3,7 +3,7 @@ package cmd import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" "github.com/CircleCI-Public/circleci-cli/api" @@ -678,7 +678,7 @@ The following orb versions already exist: ('namespace1/orb@0.0.3') ` - actual, err := ioutil.ReadAll(&b) + actual, err := io.ReadAll(&b) Expect(err).ShouldNot(HaveOccurred()) Expect(string(actual)).To(Equal(expOutput)) }) diff --git a/cmd/policy/policy.go b/cmd/policy/policy.go index c2d989156..d225347b4 100644 --- a/cmd/policy/policy.go +++ b/cmd/policy/policy.go @@ -447,7 +447,7 @@ This group of commands allows the management of polices to be verified against b runner, err := tester.NewRunner(runnerOpts) if err != nil { - return fmt.Errorf("cannot instantite runner: %w", err) + return fmt.Errorf("cannot instantiate runner: %w", err) } handlerOpts := tester.ResultHandlerOptions{ diff --git a/cmd/query.go b/cmd/query.go index 14a833642..e871426c6 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -3,7 +3,7 @@ package cmd import ( "encoding/json" "fmt" - "io/ioutil" + "io" "os" "github.com/CircleCI-Public/circleci-cli/api/graphql" @@ -51,9 +51,9 @@ func query(opts queryOptions) error { var resp map[string]interface{} if opts.args[0] == "-" { - q, err = ioutil.ReadAll(os.Stdin) + q, err = io.ReadAll(os.Stdin) } else { - q, err = ioutil.ReadFile(opts.args[0]) + q, err = os.ReadFile(opts.args[0]) } if err != nil { diff --git a/cmd/root.go b/cmd/root.go index 7f7ed4ec1..a6257771f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,11 +17,11 @@ import ( "github.com/CircleCI-Public/circleci-cli/version" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" + "golang.org/x/term" ) var defaultEndpoint = "graphql-unstable" var defaultHost = "https://circleci.com" -var defaultAPIHost = "https://api.circleci.com" var defaultRestEndpoint = "api/v2" var trueString = "true" @@ -104,11 +104,6 @@ func MakeCommands() *cobra.Command { RestEndpoint: defaultRestEndpoint, Endpoint: defaultEndpoint, GitHubAPI: "https://api.github.com/", - // The config api host differs for both cloud and server setups. - // For cloud, the base domain will be https://api.circleci.com - // for server, this should match the host as we don't have the same - // api subdomain setup - ConfigAPIHost: defaultAPIHost, } if err := rootOptions.Load(); err != nil { @@ -117,9 +112,16 @@ func MakeCommands() *cobra.Command { rootOptions.Data = &data.Data + helpWidth := getHelpWidth() + // CircleCI Logo will only appear with enough window width + longHelp := "" + if helpWidth > 85 { + longHelp = rootHelpLong() + } + rootCmd = &cobra.Command{ Use: "circleci", - Long: rootHelpLong(), + Long: longHelp, Short: rootHelpShort(rootOptions), PersistentPreRunE: func(_ *cobra.Command, _ []string) error { return rootCmdPreRun(rootOptions) @@ -132,7 +134,7 @@ func MakeCommands() *cobra.Command { cobra.AddTemplateFunc("FormatPositionalArg", md_docs.FormatPositionalArg) if os.Getenv("TESTING") != trueString { - helpCmd := helpCmd{cmd: rootCmd} + helpCmd := helpCmd{width: helpWidth} rootCmd.SetHelpFunc(helpCmd.helpTemplate) } rootCmd.SetUsageTemplate(usageTemplate) @@ -180,7 +182,6 @@ func MakeCommands() *cobra.Command { flags.StringVar(&rootOptions.Host, "host", rootOptions.Host, "URL to your CircleCI host, also CIRCLECI_CLI_HOST") flags.StringVar(&rootOptions.Endpoint, "endpoint", rootOptions.Endpoint, "URI to your CircleCI GraphQL API endpoint") flags.StringVar(&rootOptions.GitHubAPI, "github-api", "https://api.github.com/", "Change the default endpoint to GitHub API for retrieving updates") - flags.StringVar(&rootOptions.ConfigAPIHost, "config-api-host", "https://api.circleci.com", "Change the default endpoint for the config api host") flags.BoolVar(&rootOptions.SkipUpdateCheck, "skip-update-check", skipUpdateByDefault(), "Skip the check for updates check run before every command.") hidden := []string{"github-api", "debug", "endpoint"} @@ -332,10 +333,10 @@ For more help, see the documentation here: %s`, short, config.Data.Links.CLIDocs } type helpCmd struct { - cmd *cobra.Command + width int } -// helpTemplate Building a custom help template with more finess and pizazz +// helpTemplate Building a custom help template with more finesse and pizazz func (helpCmd *helpCmd) helpTemplate(cmd *cobra.Command, s []string) { /***Styles ***/ @@ -413,9 +414,21 @@ func (helpCmd *helpCmd) helpTemplate(cmd *cobra.Command, s []string) { //Border styles borderStyle := lipgloss.NewStyle(). Padding(0, 1, 0, 1). - Width(120). + Width(helpCmd.width - 2). BorderForeground(lipgloss.AdaptiveColor{Light: `#3B6385`, Dark: `#47A359`}). Border(lipgloss.ThickBorder()) log.Println("\n" + borderStyle.Render(usageText.String()+"\n")) } + +func getHelpWidth() int { + const defaultHelpWidth = 122 + if !term.IsTerminal(0) { + return defaultHelpWidth + } + w, _, err := term.GetSize(0) + if err == nil && w < defaultHelpWidth { + return w + } + return defaultHelpWidth +} diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 8a1c025d9..591a6197d 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -2,7 +2,7 @@ package cmd_test import ( "fmt" - "io/ioutil" + "io" "os" "os/exec" "regexp" @@ -248,7 +248,7 @@ Your configuration has been saved to %s. file, err := os.Open(tempSettings.Config.Path) Expect(err).ShouldNot(HaveOccurred()) - reread, err := ioutil.ReadAll(file) + reread, err := io.ReadAll(file) Expect(err).ShouldNot(HaveOccurred()) Expect(string(reread)).To(Equal(`host: https://zomg.com endpoint: graphql-unstable diff --git a/config/collaborators.go b/config/collaborators.go new file mode 100644 index 000000000..30da51d17 --- /dev/null +++ b/config/collaborators.go @@ -0,0 +1,39 @@ +package config + +import ( + "net/url" +) + +var ( + CollaborationsPath = "me/collaborations" +) + +type CollaborationResult struct { + VcsTye string `json:"vcs_type"` + OrgSlug string `json:"slug"` + OrgName string `json:"name"` + OrgId string `json:"id"` + AvatarUrl string `json:"avatar_url"` +} + +// GetOrgCollaborations - fetches all the collaborations for a given user. +func (c *ConfigCompiler) GetOrgCollaborations() ([]CollaborationResult, error) { + req, err := c.collaboratorRestClient.NewRequest("GET", &url.URL{Path: CollaborationsPath}, nil) + if err != nil { + return nil, err + } + + var resp []CollaborationResult + _, err = c.collaboratorRestClient.DoRequest(req, &resp) + return resp, err +} + +// GetOrgIdFromSlug - converts a slug into an orgID. +func GetOrgIdFromSlug(slug string, collaborations []CollaborationResult) string { + for _, v := range collaborations { + if v.OrgSlug == slug { + return v.OrgId + } + } + return "" +} diff --git a/config/collaborators_test.go b/config/collaborators_test.go new file mode 100644 index 000000000..115c4f8c9 --- /dev/null +++ b/config/collaborators_test.go @@ -0,0 +1,43 @@ +package config + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/stretchr/testify/assert" +) + +func TestGetOrgCollaborations(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + t.Run("assert compiler has correct host", func(t *testing.T) { + assert.Equal(t, "http://"+compiler.collaboratorRestClient.BaseURL.Host, svr.URL) + }) + + t.Run("getOrgCollaborations can parse response correctly", func(t *testing.T) { + collabs, err := compiler.GetOrgCollaborations() + assert.NoError(t, err) + assert.Equal(t, 1, len(collabs)) + assert.Equal(t, "circleci", collabs[0].VcsTye) + }) + + t.Run("can fetch orgID from a slug", func(t *testing.T) { + expected := "1234" + actual := GetOrgIdFromSlug("gh/test", []CollaborationResult{{OrgSlug: "gh/test", OrgId: "1234"}}) + assert.Equal(t, expected, actual) + }) + + t.Run("returns empty if no slug match", func(t *testing.T) { + expected := "" + actual := GetOrgIdFromSlug("gh/doesntexist", []CollaborationResult{{OrgSlug: "gh/test", OrgId: "1234"}}) + assert.Equal(t, expected, actual) + }) +} diff --git a/config/commands.go b/config/commands.go new file mode 100644 index 000000000..2167e6859 --- /dev/null +++ b/config/commands.go @@ -0,0 +1,158 @@ +package config + +import ( + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +func printValues(values Values) { + for key, value := range values { + fmt.Printf("\t%s:\t%s", key, value) + } +} + +type ProcessConfigOpts struct { + ConfigPath string + OrgID string + OrgSlug string + PipelineParamsFilePath string + + VerboseOutput bool +} + +func (c *ConfigCompiler) getOrgID( + optsOrgID string, + optsOrgSlug string, +) (string, error) { + if optsOrgID == "" && optsOrgSlug == "" { + fmt.Println("No org id or slug has been provided") + return "", nil + } + + var orgID string + if strings.TrimSpace(optsOrgID) != "" { + orgID = optsOrgID + } else { + orgs, err := c.GetOrgCollaborations() + if err != nil { + return "", err + } + orgID = GetOrgIdFromSlug(optsOrgSlug, orgs) + if orgID == "" { + fmt.Println("Could not fetch a valid org-id from collaborators endpoint.") + fmt.Println("Check if you have access to this org by hitting https://circleci.com/api/v2/me/collaborations") + fmt.Println("Continuing on - private orb resolution will not work as intended") + } + } + + return orgID, nil +} + +func (c *ConfigCompiler) ProcessConfig(opts ProcessConfigOpts) error { + var response *ConfigResponse + var params Parameters + var err error + + if len(opts.PipelineParamsFilePath) > 0 { + // The 'src' value can be a filepath, or a yaml string. If the file cannot be read successfully, + // proceed with the assumption that the value is already valid yaml. + raw, err := os.ReadFile(opts.PipelineParamsFilePath) + if err != nil { + raw = []byte(opts.PipelineParamsFilePath) + } + + err = yaml.Unmarshal(raw, ¶ms) + if err != nil { + return fmt.Errorf("invalid 'pipeline-parameters' provided: %s", err.Error()) + } + } + + //if no orgId provided use org slug + values := LocalPipelineValues() + if opts.VerboseOutput { + fmt.Println("Processing config with following values") + printValues(values) + } + + orgID, err := c.getOrgID(opts.OrgID, opts.OrgSlug) + if err != nil { + return fmt.Errorf("failed to get the appropriate org-id: %s", err.Error()) + } + + response, err = c.ConfigQuery( + opts.ConfigPath, + orgID, + params, + values, + ) + if err != nil { + return err + } + + if !response.Valid { + fmt.Println(response.Errors) + return errors.New("config is invalid") + } + + fmt.Print(response.OutputYaml) + return nil +} + +type ValidateConfigOpts struct { + ConfigPath string + OrgID string + OrgSlug string + + IgnoreDeprecatedImages bool + VerboseOutput bool +} + +// The arg is actually optional, in order to support compatibility with the --path flag. +func (c *ConfigCompiler) ValidateConfig(opts ValidateConfigOpts) error { + var err error + var response *ConfigResponse + + //if no orgId provided use org slug + values := LocalPipelineValues() + if opts.VerboseOutput { + fmt.Println("Validating config with following values") + printValues(values) + } + + orgID, err := c.getOrgID(opts.OrgID, opts.OrgSlug) + if err != nil { + return fmt.Errorf("failed to get the appropriate org-id: %s", err.Error()) + } + + response, err = c.ConfigQuery( + opts.ConfigPath, + orgID, + nil, + LocalPipelineValues(), + ) + if err != nil { + return err + } + + if !response.Valid { + fmt.Println(response.Errors) + return errors.New("config is invalid") + } + + // check if a deprecated Linux VM image is being used + // link here to blog post when available + // returns an error if a deprecated image is used + if !opts.IgnoreDeprecatedImages { + err := deprecatedImageCheck(response) + if err != nil { + return err + } + } + + fmt.Printf("\nConfig file at %s is valid.\n", opts.ConfigPath) + return nil +} diff --git a/config/commands_test.go b/config/commands_test.go new file mode 100644 index 000000000..dc6e7a16a --- /dev/null +++ b/config/commands_test.go @@ -0,0 +1,163 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/stretchr/testify/assert" +) + +func TestGetOrgID(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + t.Run("returns the original org-id passed if it is set", func(t *testing.T) { + expected := "1234" + actual, err := compiler.getOrgID("1234", "") + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("returns the correct org id from org-slug", func(t *testing.T) { + expected := "2345" + actual, err := compiler.getOrgID("", "gh/test") + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("returns the correct org id with org-id and org-slug both set", func(t *testing.T) { + expected := "1234" + actual, err := compiler.getOrgID("1234", "gh/test") + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("does not return an error if org-id cannot be found", func(t *testing.T) { + expected := "" + actual, err := compiler.getOrgID("", "gh/doesntexist") + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + +} + +var testYaml = `version: 2.1\n\norbs:\n node: circleci/node@5.0.3\n\njobs:\n datadog-hello-world:\n docker:\n - image: cimg/base:stable\n steps:\n - run: |\n echo \"doing something really cool\"\nworkflows:\n datadog-hello-world:\n jobs:\n - datadog-hello-world\n` + +func TestValidateConfig(t *testing.T) { + t.Run("validate config works as expected", func(t *testing.T) { + t.Run("validate config is able to send a request with no owner-id", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + reqBody, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + var req CompileConfigRequest + err = json.Unmarshal(reqBody, &req) + assert.NoError(t, err) + fmt.Fprintf(w, `{"valid":true,"source-yaml":"%s","output-yaml":"%s","errors":[]}`, testYaml, testYaml) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + err := compiler.ValidateConfig(ValidateConfigOpts{ + ConfigPath: "testdata/config.yml", + }) + assert.NoError(t, err) + }) + + t.Run("validate config is able to send a request with owner-id", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + reqBody, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + var req CompileConfigRequest + err = json.Unmarshal(reqBody, &req) + assert.NoError(t, err) + assert.Equal(t, "1234", req.Options.OwnerID) + fmt.Fprintf(w, `{"valid":true,"source-yaml":"%s","output-yaml":"%s","errors":[]}`, testYaml, testYaml) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + err := compiler.ValidateConfig(ValidateConfigOpts{ + ConfigPath: "testdata/config.yml", + OrgID: "1234", + }) + assert.NoError(t, err) + }) + + t.Run("validate config is able to send a request with owner-id from slug", func(t *testing.T) { + mux := http.NewServeMux() + + mux.HandleFunc("/compile-config-with-defaults", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + reqBody, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + var req CompileConfigRequest + err = json.Unmarshal(reqBody, &req) + assert.NoError(t, err) + assert.Equal(t, "2345", req.Options.OwnerID) + fmt.Fprintf(w, `{"valid":true,"source-yaml":"%s","output-yaml":"%s","errors":[]}`, testYaml, testYaml) + }) + + mux.HandleFunc("/me/collaborations", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) + }) + + svr := httptest.NewServer(mux) + defer svr.Close() + + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + err := compiler.ValidateConfig(ValidateConfigOpts{ + ConfigPath: "testdata/config.yml", + OrgSlug: "gh/test", + }) + assert.NoError(t, err) + }) + + t.Run("validate config is able to send a request with no owner-id after failed collaborations lookup", func(t *testing.T) { + mux := http.NewServeMux() + + mux.HandleFunc("/compile-config-with-defaults", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + reqBody, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + var req CompileConfigRequest + err = json.Unmarshal(reqBody, &req) + assert.NoError(t, err) + assert.Equal(t, "", req.Options.OwnerID) + fmt.Fprintf(w, `{"valid":true,"source-yaml":"%s","output-yaml":"%s","errors":[]}`, testYaml, testYaml) + }) + + mux.HandleFunc("/me/collaborations", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) + }) + + svr := httptest.NewServer(mux) + defer svr.Close() + + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + err := compiler.ValidateConfig(ValidateConfigOpts{ + ConfigPath: "testdata/config.yml", + OrgSlug: "gh/nonexistent", + }) + assert.NoError(t, err) + }) + }) +} diff --git a/config/config.go b/config/config.go index 268e23467..7d17b75e5 100644 --- a/config/config.go +++ b/config/config.go @@ -2,19 +2,52 @@ package config import ( "fmt" - "io/ioutil" + "io" "net/url" "os" "github.com/CircleCI-Public/circleci-cli/api/rest" - "github.com/CircleCI-Public/circleci-cli/pipeline" + "github.com/CircleCI-Public/circleci-cli/settings" "github.com/pkg/errors" ) +var ( + defaultHost = "https://circleci.com" + defaultAPIHost = "https://api.circleci.com" + + // Making this the one true source for default config path + DefaultConfigPath = ".circleci/config.yml" +) + +type ConfigCompiler struct { + host string + compileRestClient *rest.Client + collaboratorRestClient *rest.Client +} + +func New(cfg *settings.Config) *ConfigCompiler { + hostValue := getCompileHost(cfg.Host) + configCompiler := &ConfigCompiler{ + host: hostValue, + compileRestClient: rest.NewFromConfig(hostValue, cfg), + collaboratorRestClient: rest.NewFromConfig(cfg.Host, cfg), + } + return configCompiler +} + +func getCompileHost(cfgHost string) string { + if cfgHost != defaultHost { + return cfgHost + } else { + return defaultAPIHost + } +} + type ConfigError struct { Message string `json:"message"` } +// ConfigResponse - the structure of what is returned from the downstream compilation endpoint type ConfigResponse struct { Valid bool `json:"valid"` SourceYaml string `json:"source-yaml"` @@ -22,6 +55,7 @@ type ConfigResponse struct { Errors []ConfigError `json:"errors"` } +// CompileConfigRequest - the structure of the data we send to the downstream compilation service. type CompileConfigRequest struct { ConfigYaml string `json:"config_yaml"` Options Options `json:"options"` @@ -33,55 +67,30 @@ type Options struct { PipelineValues map[string]string `json:"pipeline_values,omitempty"` } -// #nosec -func loadYaml(path string) (string, error) { - var err error - var config []byte - if path == "-" { - config, err = ioutil.ReadAll(os.Stdin) - } else { - config, err = ioutil.ReadFile(path) - } - - if err != nil { - return "", errors.Wrapf(err, "Could not load config file at %s", path) - } - - return string(config), nil -} - // ConfigQuery - attempts to compile or validate a given config file with the // passed in params/values. // If the orgID is passed in, the config-compilation with private orbs should work. -func ConfigQuery( - rest *rest.Client, +func (c *ConfigCompiler) ConfigQuery( configPath string, orgID string, - params pipeline.Parameters, - values pipeline.Values, + params Parameters, + values Values, ) (*ConfigResponse, error) { - configString, err := loadYaml(configPath) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to load yaml config from config path provider: %w", err) } compileRequest := CompileConfigRequest{ ConfigYaml: configString, Options: Options{ - PipelineValues: values, + OwnerID: orgID, + PipelineValues: values, + PipelineParameters: params, }, } - if orgID != "" { - compileRequest.Options.OwnerID = orgID - } - - if len(params) >= 1 { - compileRequest.Options.PipelineParameters = params - } - - req, err := rest.NewRequest( + req, err := c.compileRestClient.NewRequest( "POST", &url.URL{ Path: "compile-config-with-defaults", @@ -89,21 +98,41 @@ func ConfigQuery( compileRequest, ) if err != nil { - return nil, err + return nil, fmt.Errorf("an error occurred creating the request: %w", err) } configCompilationResp := &ConfigResponse{} - statusCode, err := rest.DoRequest(req, configCompilationResp) + statusCode, err := c.compileRestClient.DoRequest(req, configCompilationResp) if err != nil { - return nil, err + if statusCode == 404 { + return nil, errors.New("this version of the CLI does not support your instance of server, please refer to https://github.com/CircleCI-Public/circleci-cli for version compatibility") + } + return nil, fmt.Errorf("config compilation request returned an error: %w", err) } + if statusCode != 200 { return nil, errors.New("unable to validate or compile config") } if len(configCompilationResp.Errors) > 0 { - return nil, errors.New(fmt.Sprintf("config compilation contains errors: %s", configCompilationResp.Errors)) + return nil, fmt.Errorf("config compilation contains errors: %s", configCompilationResp.Errors) } return configCompilationResp, nil } + +func loadYaml(path string) (string, error) { + var err error + var config []byte + if path == "-" { + config, err = io.ReadAll(os.Stdin) + } else { + config, err = os.ReadFile(path) + } + + if err != nil { + return "", errors.Wrapf(err, "Could not load config file at %s", path) + } + + return string(config), nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 000000000..deeba3c00 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,140 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/stretchr/testify/assert" +) + +func TestCompiler(t *testing.T) { + t.Run("test compiler setup", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + t.Run("assert compiler has correct host", func(t *testing.T) { + assert.Equal(t, "http://"+compiler.compileRestClient.BaseURL.Host, svr.URL) + }) + + t.Run("assert compiler has default api host", func(t *testing.T) { + newCompiler := New(&settings.Config{Host: defaultHost, HTTPClient: http.DefaultClient}) + assert.Equal(t, "https://"+newCompiler.compileRestClient.BaseURL.Host, defaultAPIHost) + }) + + t.Run("tests that we correctly get the config api host when the host is not the default one", func(t *testing.T) { + // if the host isn't equal to `https://circleci.com` then this is likely a server instance and + // wont have the api.X.com subdomain so we should instead just respect the host for config commands + host := getCompileHost("test") + assert.Equal(t, host, "test") + + // If the host passed in is the same as the defaultHost 'https://circleci.com' - then we know this is cloud + // and as such should use the `api.circleci.com` subdomain + host = getCompileHost("https://circleci.com") + assert.Equal(t, host, "https://api.circleci.com") + }) + }) + + t.Run("test ConfigQuery", func(t *testing.T) { + t.Run("returns the correct configCompilation response", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"valid":true,"source-yaml":"source","output-yaml":"output","errors":[]}`) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + result, err := compiler.ConfigQuery("testdata/config.yml", "1234", Parameters{}, Values{}) + assert.NoError(t, err) + assert.Equal(t, true, result.Valid) + assert.Equal(t, "output", result.OutputYaml) + assert.Equal(t, "source", result.SourceYaml) + }) + + t.Run("returns error when config file could not be found", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"valid":true,"source-yaml":"source","output-yaml":"output","errors":[]}`) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + _, err := compiler.ConfigQuery("testdata/nonexistent.yml", "1234", Parameters{}, Values{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Could not load config file at testdata/nonexistent.yml") + }) + + t.Run("handles 404 status correctly", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + _, err := compiler.ConfigQuery("testdata/config.yml", "1234", Parameters{}, Values{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "this version of the CLI does not support your instance of server") + }) + + t.Run("handles non-200 status correctly", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + _, err := compiler.ConfigQuery("testdata/config.yml", "1234", Parameters{}, Values{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config compilation request returned an error") + }) + + t.Run("server gets correct information owner ID", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + reqBody, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + var req CompileConfigRequest + err = json.Unmarshal(reqBody, &req) + assert.NoError(t, err) + assert.Equal(t, "1234", req.Options.OwnerID) + assert.Equal(t, "test: test\n", req.ConfigYaml) + fmt.Fprintf(w, `{"valid":true,"source-yaml":"source","output-yaml":"output","errors":[]}`) + })) + defer svr.Close() + compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) + + resp, err := compiler.ConfigQuery("testdata/test.yml", "1234", Parameters{}, Values{}) + assert.NoError(t, err) + assert.Equal(t, true, resp.Valid) + assert.Equal(t, "output", resp.OutputYaml) + assert.Equal(t, "source", resp.SourceYaml) + }) + + }) + +} + +func TestLoadYaml(t *testing.T) { + t.Run("tests load yaml", func(t *testing.T) { + expected := `test: test +` + actual, err := loadYaml("testdata/test.yml") + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("returns error for non-existent yml file", func(t *testing.T) { + actual, err := loadYaml("testdata/non-existent.yml") + assert.Error(t, err) + assert.Equal(t, "", actual) + }) +} diff --git a/config/deprecated-images.go b/config/deprecated.go similarity index 96% rename from config/deprecated-images.go rename to config/deprecated.go index fcc998fc9..fc8501ae0 100644 --- a/config/deprecated-images.go +++ b/config/deprecated.go @@ -38,8 +38,7 @@ type processedConfig struct { } // Processes the config down to v2.0, then checks image used against the block list -func DeprecatedImageCheck(response *ConfigResponse) error { - +func deprecatedImageCheck(response *ConfigResponse) error { aConfig := processedConfig{} err := yaml.Unmarshal([]byte(response.OutputYaml), &aConfig) if err != nil { diff --git a/config/deprecated_test.go b/config/deprecated_test.go new file mode 100644 index 000000000..8f7c53c00 --- /dev/null +++ b/config/deprecated_test.go @@ -0,0 +1,35 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var TestErrorCase = ` +jobs: + job: + machine: circleci/classic:201710-01 +` + +var TestHappyCase = ` +jobs: + job: + image: non/deprecated +` + +func TestDeprecatedImageCheck(t *testing.T) { + t.Run("happy path - tests deprecated image check works", func(t *testing.T) { + err := deprecatedImageCheck(&ConfigResponse{ + OutputYaml: TestErrorCase, + }) + assert.Error(t, err) + }) + + t.Run("happy path - no error if image used", func(t *testing.T) { + err := deprecatedImageCheck(&ConfigResponse{ + OutputYaml: TestHappyCase, + }) + assert.Nil(t, err) + }) +} diff --git a/pipeline/pipeline.go b/config/pipeline.go similarity index 64% rename from pipeline/pipeline.go rename to config/pipeline.go index 7daefa96b..09375d918 100644 --- a/pipeline/pipeline.go +++ b/config/pipeline.go @@ -1,8 +1,7 @@ -package pipeline +package config import ( "fmt" - "sort" "github.com/CircleCI-Public/circleci-cli/git" ) @@ -45,27 +44,3 @@ func LocalPipelineValues() Values { return vals } - -// TODO: type Parameters map[string]string - -// KeyVal is a data structure specifically for passing pipeline data to GraphQL which doesn't support free-form maps. -type KeyVal struct { - Key string `json:"key"` - Val string `json:"val"` -} - -// PrepareForGraphQL takes a golang homogenous map, and transforms it into a list of keyval pairs, since GraphQL does not support homogenous maps. -func PrepareForGraphQL(kvMap Values) []KeyVal { - // we need to create the slice of KeyVals in a deterministic order for testing purposes - keys := make([]string, 0, len(kvMap)) - for k := range kvMap { - keys = append(keys, k) - } - sort.Strings(keys) - - kvs := make([]KeyVal, 0, len(kvMap)) - for _, k := range keys { - kvs = append(kvs, KeyVal{Key: k, Val: kvMap[k]}) - } - return kvs -} diff --git a/config/testdata/config-no-orb.yml b/config/testdata/config-no-orb.yml new file mode 100644 index 000000000..35f2573f2 --- /dev/null +++ b/config/testdata/config-no-orb.yml @@ -0,0 +1,13 @@ +version: 2.1 + +jobs: + datadog-hello-world: + docker: + - image: cimg/base:stable + steps: + - run: | + echo "doing something really cool" +workflows: + datadog-hello-world: + jobs: + - datadog-hello-world diff --git a/config/testdata/config.yml b/config/testdata/config.yml new file mode 100644 index 000000000..d5f89b865 --- /dev/null +++ b/config/testdata/config.yml @@ -0,0 +1,16 @@ +version: 2.1 + +orbs: + node: circleci/node@5.0.3 + +jobs: + datadog-hello-world: + docker: + - image: cimg/base:stable + steps: + - run: | + echo "doing something really cool" +workflows: + datadog-hello-world: + jobs: + - datadog-hello-world diff --git a/config/testdata/test.yml b/config/testdata/test.yml new file mode 100644 index 000000000..e5239010e --- /dev/null +++ b/config/testdata/test.yml @@ -0,0 +1 @@ +test: test diff --git a/filetree/filetree.go b/filetree/filetree.go index f8c64e110..9de10d4d5 100644 --- a/filetree/filetree.go +++ b/filetree/filetree.go @@ -2,7 +2,6 @@ package filetree import ( "fmt" - "io/ioutil" "os" "path/filepath" "regexp" @@ -117,7 +116,7 @@ func (n Node) marshalLeaf() (interface{}, error) { return content, nil } - buf, err := ioutil.ReadFile(n.FullPath) + buf, err := os.ReadFile(n.FullPath) if err != nil { return content, err diff --git a/filetree/filetree_test.go b/filetree/filetree_test.go index 3bf258415..56847f6ff 100644 --- a/filetree/filetree_test.go +++ b/filetree/filetree_test.go @@ -1,7 +1,6 @@ package filetree_test import ( - "io/ioutil" "os" "path/filepath" "sort" @@ -22,7 +21,7 @@ var _ = Describe("filetree", func() { BeforeEach(func() { var err error - tempRoot, err = ioutil.TempDir("", "circleci-cli-test-") + tempRoot, err = os.MkdirTemp("", "circleci-cli-test-") Expect(err).ToNot(HaveOccurred()) }) @@ -39,7 +38,7 @@ var _ = Describe("filetree", func() { emptyDir = filepath.Join(tempRoot, "empty_dir") Expect(os.Mkdir(subDir, 0700)).To(Succeed()) - Expect(ioutil.WriteFile(subDirFile, []byte("foo:\n bar:\n baz"), 0600)).To(Succeed()) + Expect(os.WriteFile(subDirFile, []byte("foo:\n bar:\n baz"), 0600)).To(Succeed()) Expect(os.Mkdir(emptyDir, 0700)).To(Succeed()) }) @@ -48,7 +47,7 @@ var _ = Describe("filetree", func() { anotherDir := filepath.Join(tempRoot, "another_dir") anotherDirFile := filepath.Join(tempRoot, "another_dir", "another_dir_file.yml") Expect(os.Mkdir(anotherDir, 0700)).To(Succeed()) - Expect(ioutil.WriteFile(anotherDirFile, []byte("1some: in: valid: yaml"), 0600)).To(Succeed()) + Expect(os.WriteFile(anotherDirFile, []byte("1some: in: valid: yaml"), 0600)).To(Succeed()) tree, err := filetree.NewTree(tempRoot) Expect(err).ToNot(HaveOccurred()) @@ -118,7 +117,7 @@ sub_dir: emptyDir = filepath.Join(tempRoot, "empty_dir") Expect(os.Mkdir(subDir, 0700)).To(Succeed()) - Expect(ioutil.WriteFile(subDirFile, []byte("foo:\n bar:\n baz"), 0600)).To(Succeed()) + Expect(os.WriteFile(subDirFile, []byte("foo:\n bar:\n baz"), 0600)).To(Succeed()) Expect(os.Mkdir(emptyDir, 0700)).To(Succeed()) }) diff --git a/integration_tests/features/circleci_config.feature b/integration_tests/features/circleci_config.feature index 439103b78..3f645f936 100644 --- a/integration_tests/features/circleci_config.feature +++ b/integration_tests/features/circleci_config.feature @@ -56,7 +56,7 @@ Feature: Config checking jobs: - datadog-hello-world """ - When I run `circleci --config-api-host https://k9s.sphereci.com config validate --skip-update-check -c config.yml` + When I run `circleci --host https://k9s.sphereci.com config validate --skip-update-check -c config.yml` Then the exit status should be 0 And the output should contain "Config file at config.yml is valid" diff --git a/local/local.go b/local/local.go index 845f5b33b..320789c66 100644 --- a/local/local.go +++ b/local/local.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "os" "os/exec" "path" @@ -12,9 +11,7 @@ import ( "strings" "syscall" - "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/config" - "github.com/CircleCI-Public/circleci-cli/pipeline" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/pkg/errors" "github.com/spf13/pflag" @@ -27,20 +24,20 @@ const DefaultConfigPath = ".circleci/config.yml" func Execute(flags *pflag.FlagSet, cfg *settings.Config, args []string) error { var err error var configResponse *config.ConfigResponse - restClient := rest.NewFromConfig(cfg.Host, cfg) - processedArgs, configPath := buildAgentArguments(flags) + compiler := config.New(cfg) + //if no orgId provided use org slug orgID, _ := flags.GetString("org-id") if strings.TrimSpace(orgID) != "" { - configResponse, err = config.ConfigQuery(restClient, configPath, orgID, nil, pipeline.LocalPipelineValues()) + configResponse, err = compiler.ConfigQuery(configPath, orgID, nil, config.LocalPipelineValues()) if err != nil { return err } } else { orgSlug, _ := flags.GetString("org-slug") - configResponse, err = config.ConfigQuery(restClient, configPath, orgSlug, nil, pipeline.LocalPipelineValues()) + configResponse, err = compiler.ConfigQuery(configPath, orgSlug, nil, config.LocalPipelineValues()) if err != nil { return err } @@ -262,7 +259,7 @@ func writeStringToTempFile(data string) (string, error) { // > The path /var/folders/q0/2g2lcf6j79df6vxqm0cg_0zm0000gn/T/287575618-config.yml // > is not shared from OS X and is not known to Docker. // Docker has `/tmp` shared by default. - f, err := ioutil.TempFile("/tmp", "*_circleci_config.yml") + f, err := os.CreateTemp("/tmp", "*_circleci_config.yml") if err != nil { return "", errors.Wrap(err, "Error creating temporary config file") diff --git a/local/local_test.go b/local/local_test.go index c316f045b..598806ca9 100644 --- a/local/local_test.go +++ b/local/local_test.go @@ -1,7 +1,7 @@ package local import ( - "io/ioutil" + "io" "os" . "github.com/onsi/ginkgo" @@ -39,7 +39,7 @@ var _ = Describe("build", func() { path, err := writeStringToTempFile("cynosure") Expect(err).NotTo(HaveOccurred()) defer os.Remove(path) - Expect(ioutil.ReadFile(path)).To(BeEquivalentTo("cynosure")) + Expect(os.ReadFile(path)).To(BeEquivalentTo("cynosure")) }) }) @@ -51,7 +51,7 @@ var _ = Describe("build", func() { // add a 'debug' flag - the build command will inherit this from the // root command when not testing in isolation. flags.Bool("debug", false, "Enable debug logging.") - flags.SetOutput(ioutil.Discard) + flags.SetOutput(io.Discard) err := flags.Parse(args) return flags, err } diff --git a/mock/http.go b/mock/http.go index 8f93a6a81..f905ca0a3 100644 --- a/mock/http.go +++ b/mock/http.go @@ -1,7 +1,7 @@ package mock import ( - "io/ioutil" + "io" "net/http" "strings" ) @@ -23,6 +23,6 @@ func NewHTTPClient(f func(*http.Request) (*http.Response, error)) *http.Client { func NewHTTPResponse(code int, body string) *http.Response { return &http.Response{ StatusCode: code, - Body: ioutil.NopCloser(strings.NewReader(body)), + Body: io.NopCloser(strings.NewReader(body)), } } diff --git a/process/process.go b/process/process.go index cb7745ae1..216f9ad69 100644 --- a/process/process.go +++ b/process/process.go @@ -2,7 +2,7 @@ package process import ( "fmt" - "io/ioutil" + "os" "path/filepath" "regexp" "strings" @@ -31,7 +31,7 @@ func MaybeIncludeFile(s string, orbDirectory string) (string, error) { } filepath := filepath.Join(orbDirectory, subMatch) - file, err := ioutil.ReadFile(filepath) + file, err := os.ReadFile(filepath) if err != nil { return "", fmt.Errorf("could not open %s for inclusion", filepath) } diff --git a/settings/settings.go b/settings/settings.go index ce542af26..36928fac5 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "errors" "fmt" - "io/ioutil" "net/http" "net/url" "os" @@ -37,10 +36,6 @@ type Config struct { GitHubAPI string `yaml:"-"` SkipUpdateCheck bool `yaml:"-"` OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"` - // Represents the API host we want to use for config compilation and validation - // requests - this is typically on the api.circleci.com subdomain for cloud, or the - // same domain for server instances. - ConfigAPIHost string `yaml:"-"` } type OrbPublishingInfo struct { @@ -65,7 +60,7 @@ func (upd *UpdateCheck) Load() error { upd.FileUsed = path - content, err := ioutil.ReadFile(path) // #nosec + content, err := os.ReadFile(path) // #nosec if err != nil { return err } @@ -81,7 +76,7 @@ func (upd *UpdateCheck) WriteToDisk() error { return err } - err = ioutil.WriteFile(upd.FileUsed, enc, 0600) + err = os.WriteFile(upd.FileUsed, enc, 0600) return err } @@ -106,7 +101,7 @@ func (cfg *Config) LoadFromDisk() error { cfg.FileUsed = path - content, err := ioutil.ReadFile(path) // #nosec + content, err := os.ReadFile(path) // #nosec if err != nil { return err } @@ -126,7 +121,7 @@ func (cfg *Config) WriteToDisk() error { return err } - err = ioutil.WriteFile(cfg.FileUsed, enc, 0600) + err = os.WriteFile(cfg.FileUsed, enc, 0600) return err } @@ -134,10 +129,6 @@ func (cfg *Config) WriteToDisk() error { func (cfg *Config) LoadFromEnv(prefix string) { if host := ReadFromEnv(prefix, "host"); host != "" { cfg.Host = host - // If the user is a server customer and overwrites the default - // https://circleci.com host - we then have to use this as the host for - // any config compilation or validation requests as opposed to https://api.circleci.com - cfg.ConfigAPIHost = host } if restEndpoint := ReadFromEnv(prefix, "rest_endpoint"); restEndpoint != "" { @@ -220,7 +211,7 @@ func (cfg *Config) WithHTTPClient() error { return fmt.Errorf("invalid tls cert provided: %s", err.Error()) } - pemData, err := ioutil.ReadFile(cfg.TLSCert) + pemData, err := os.ReadFile(cfg.TLSCert) if err != nil { return fmt.Errorf("unable to read tls cert: %s", err.Error()) }