Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIPE-3873 Code architectural cleanup #884

Merged
merged 4 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,8 @@ 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 homebrew 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.
elliotforbes marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 3 additions & 3 deletions api/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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
}
Expand Down
250 changes: 48 additions & 202 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,16 @@ package cmd

import (
"fmt"
"net/url"
"os"
"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
Expand All @@ -39,19 +22,7 @@ var configAnnotations = map[string]string{
"<path>": "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",
Expand All @@ -60,11 +31,8 @@ func newConfigCommand(config *settings.Config) *cobra.Command {
packCommand := &cobra.Command{
Use: "pack <path>",
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),
Expand All @@ -75,12 +43,24 @@ func newConfigCommand(config *settings.Config) *cobra.Command {
Use: "validate <path>",
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 {
corinnesollows marked this conversation as resolved.
Show resolved Hide resolved
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),
Expand All @@ -89,6 +69,7 @@ func newConfigCommand(config *settings.Config) *cobra.Command {
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)
}
Expand All @@ -98,12 +79,25 @@ func newConfigCommand(config *settings.Config) *cobra.Command {
processCommand := &cobra.Command{
Use: "process <path>",
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),
Expand All @@ -112,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,
Expand All @@ -138,115 +129,8 @@ func newConfigCommand(config *settings.Config) *cobra.Command {
return configCmd
}

// The <path> 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()
if verboseOutput {
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 := os.ReadFile(paramsYaml)
if err != nil {
raw = []byte(paramsYaml)
}

err = yaml.Unmarshal(raw, &params)
if err != nil {
return fmt.Errorf("invalid 'pipeline-parameters' provided: %s", err.Error())
}
}

//if no orgId provided use org slug
values := pipeline.LocalPipelineValues()
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])
corinnesollows marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return errors.Wrap(err, "An error occurred trying to build the tree")
}
Expand All @@ -259,44 +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) {
fmt.Fprintln(os.Stderr, "Processing config with following values:")
for key, value := range values {
fmt.Fprintf(os.Stderr, "%-18s %s\n", 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)
}
Loading