From 22614a1acd3c415af7def78e1e7e3961945923bb Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Tue, 14 Jan 2025 17:57:20 -0800 Subject: [PATCH 01/10] init --- cmd/ctrlc/root/root.go | 2 + cmd/ctrlc/root/sync/sync.go | 5 +- cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go | 134 ++++++++++++++++++ cmd/ctrlc/root/sync/tfe/tfe.go | 61 ++++++++ go.mod | 11 +- go.sum | 24 ++++ 6 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go create mode 100644 cmd/ctrlc/root/sync/tfe/tfe.go diff --git a/cmd/ctrlc/root/root.go b/cmd/ctrlc/root/root.go index 963e952..5f8b2f5 100644 --- a/cmd/ctrlc/root/root.go +++ b/cmd/ctrlc/root/root.go @@ -7,6 +7,7 @@ import ( "github.com/ctrlplanedev/cli/cmd/ctrlc/root/agent" "github.com/ctrlplanedev/cli/cmd/ctrlc/root/api" "github.com/ctrlplanedev/cli/cmd/ctrlc/root/config" + "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync" "github.com/spf13/cobra" ) @@ -44,6 +45,7 @@ func NewRootCmd() *cobra.Command { cmd.AddCommand(agent.NewAgentCmd()) cmd.AddCommand(api.NewAPICmd()) cmd.AddCommand(config.NewConfigCmd()) + cmd.AddCommand(sync.NewSyncCmd()) return cmd } diff --git a/cmd/ctrlc/root/sync/sync.go b/cmd/ctrlc/root/sync/sync.go index 58a71eb..798d32e 100644 --- a/cmd/ctrlc/root/sync/sync.go +++ b/cmd/ctrlc/root/sync/sync.go @@ -2,10 +2,11 @@ package sync import ( "github.com/MakeNowJust/heredoc/v2" + "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/tfe" "github.com/spf13/cobra" ) -func NewRootCmd() *cobra.Command { +func NewSyncCmd() *cobra.Command { cmd := &cobra.Command{ Use: "sync ", Short: "Sync resources into Ctrlplane", @@ -15,5 +16,7 @@ func NewRootCmd() *cobra.Command { `), } + cmd.AddCommand(tfe.NewSyncTfeCmd()) + return cmd } diff --git a/cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go b/cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go new file mode 100644 index 0000000..de93885 --- /dev/null +++ b/cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go @@ -0,0 +1,134 @@ +package tfe + +import ( + "context" + "encoding/json" + "fmt" + + "strconv" + + "github.com/charmbracelet/log" + "github.com/hashicorp/go-tfe" +) + +const ( + Kind = "Workspace" +) + +type WorkspaceResource struct { + Version string + Kind string + Name string + Identifier string + Config map[string]string + Metadata map[string]string +} + +func getLinksMetadata(workspace *tfe.Workspace, baseURL string) *string { + if workspace.Organization == nil { + return nil + } + links := map[string]string{ + "Terraform Workspace": fmt.Sprintf("%s/app/%s/workspaces/%s", baseURL, workspace.Organization.Name, workspace.Name), + } + linksJSON, err := json.Marshal(links) + if err != nil { + log.Error("Failed to marshal links", "error", err) + return nil + } + linksString := string(linksJSON) + return &linksString +} + +func getWorkspaceVariables(workspace *tfe.Workspace) map[string]string { + variables := make(map[string]string) + for _, variable := range workspace.Variables { + if variable != nil && variable.Category == tfe.CategoryTerraform && !variable.Sensitive { + key := fmt.Sprintf("terraform-cloud/variables/%s", variable.Key) + variables[key] = variable.Value + } + } + return variables +} + +func getWorkspaceVcsRepo(workspace *tfe.Workspace) map[string]string { + vcsRepo := make(map[string]string) + if workspace.VCSRepo != nil { + vcsRepo["terraform-cloud/vcs-repo/identifier"] = workspace.VCSRepo.Identifier + vcsRepo["terraform-cloud/vcs-repo/branch"] = workspace.VCSRepo.Branch + vcsRepo["terraform-cloud/vcs-repo/repository-http-url"] = workspace.VCSRepo.RepositoryHTTPURL + } + return vcsRepo +} + +func getWorkspaceTags(workspace *tfe.Workspace) map[string]string { + tags := make(map[string]string) + for _, tag := range workspace.Tags { + key := fmt.Sprintf("terraform-cloud/tag/%s", tag.Name) + tags[key] = "true" + } + return tags +} + +func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL string) (WorkspaceResource, error) { + if workspace == nil { + return WorkspaceResource{}, fmt.Errorf("workspace is nil") + } + version := workspace.TerraformVersion + kind := Kind + name := workspace.Name + identifier := workspace.ID + config := map[string]string{ + "workspaceId": workspace.ID, + } + metadata := map[string]string{ + "ctrlplane/external-id": workspace.ID, + "terraform-cloud/organization": workspace.Organization.Name, + "terraform-cloud/workspace-name": workspace.Name, + "terraform-cloud/workspace-auto-apply": strconv.FormatBool(workspace.AutoApply), + "terraform/version": workspace.TerraformVersion, + } + + linksMetadata := getLinksMetadata(workspace, baseURL) + if linksMetadata != nil { + metadata["ctrlplane/links"] = *linksMetadata + } + + variables := getWorkspaceVariables(workspace) + for key, value := range variables { + metadata[key] = value + } + + tags := getWorkspaceTags(workspace) + for key, value := range tags { + metadata[key] = value + } + + vcsRepo := getWorkspaceVcsRepo(workspace) + for key, value := range vcsRepo { + metadata[key] = value + } + + return WorkspaceResource{ + Version: version, + Kind: kind, + Name: name, + Identifier: identifier, + Config: config, + Metadata: metadata, + }, nil +} + +func getWorkspacesInOrg(client *tfe.Client, organization string) ([]*tfe.Workspace, error) { + + // TODO: use cmd context + ctx := context.Background() + + items, err := client.Workspaces.List(ctx, organization, &tfe.WorkspaceListOptions{}) + if err != nil { + return nil, err + } + + workspaces := items.Items + return workspaces, nil +} diff --git a/cmd/ctrlc/root/sync/tfe/tfe.go b/cmd/ctrlc/root/sync/tfe/tfe.go new file mode 100644 index 0000000..1757b22 --- /dev/null +++ b/cmd/ctrlc/root/sync/tfe/tfe.go @@ -0,0 +1,61 @@ +package tfe + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/ctrlplanedev/cli/internal/api" + "github.com/hashicorp/go-tfe" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func validateArgs(organization, workspace string) error { + if organization == "" && workspace == "" { + return fmt.Errorf("either organization or workspace must be provided") + } + if organization != "" && workspace != "" { + return fmt.Errorf("only one of organization or workspace can be provided") + } + return nil +} + +func NewSyncTfeCmd() *cobra.Command { + var ctrlplaneWorkspaceId string + var organization string + var workspace string + + cmd := &cobra.Command{ + Use: "tfe", + Short: "Sync TFE resources into Ctrlplane", + Example: heredoc.Doc(` + # Sync all workspaces in an organization + $ ctrlc sync tfe --organization my-org --ctrlplane-workspace-id 1234567890 + + # Sync a specific workspace + $ ctrlc sync tfe --workspace 1234567890 --ctrlplane-workspace-id 1234567890 + `), + RunE: func(cmd *cobra.Command, args []string) error { + apiURL := viper.GetString("url") + apiKey := viper.GetString("api-key") + + client, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey) + if err != nil { + return fmt.Errorf("failed to create API client: %w", err) + } + + if err := validateArgs(organization, workspace); err != nil { + return fmt.Errorf("invalid arguments: %w", err) + } + + tfeClient, err := tfe.NewClient(tfe.DefaultConfig()) + if err != nil { + return fmt.Errorf("failed to create TFE client: %w", err) + } + + return nil + }, + } + + return cmd +} diff --git a/go.mod b/go.mod index fa9e3e9..cd35418 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,14 @@ require ( github.com/charmbracelet/log v0.4.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-tfe v1.73.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/jsonapi v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -44,8 +51,10 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.6.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7d67ccd..971f436 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRr github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -25,14 +27,29 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-tfe v1.73.1 h1:JmS5OClA4SkW8MygjgbSovYc8W4rPCUpxqXWEUVsOII= +github.com/hashicorp/go-tfe v1.73.1/go.mod h1:1+oOnpyJ+I/shr8GV+pdB8wzitFWO9p1VOkDhUSlyj0= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= +github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -110,12 +127,19 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 771f29998b96df8364cdc71ecfc926afd3239f25 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 00:13:27 -0800 Subject: [PATCH 02/10] more --- cmd/ctrlc/root/sync/sync.go | 5 +- .../terraform-org-workspaces.go} | 101 +++++++++++----- cmd/ctrlc/root/sync/terraform/terraform.go | 111 ++++++++++++++++++ cmd/ctrlc/root/sync/tfe/tfe.go | 61 ---------- go.mod | 1 + 5 files changed, 188 insertions(+), 91 deletions(-) rename cmd/ctrlc/root/sync/{tfe/tfe-org-workspaces.go => terraform/terraform-org-workspaces.go} (54%) create mode 100644 cmd/ctrlc/root/sync/terraform/terraform.go delete mode 100644 cmd/ctrlc/root/sync/tfe/tfe.go diff --git a/cmd/ctrlc/root/sync/sync.go b/cmd/ctrlc/root/sync/sync.go index 798d32e..88e69f5 100644 --- a/cmd/ctrlc/root/sync/sync.go +++ b/cmd/ctrlc/root/sync/sync.go @@ -2,7 +2,7 @@ package sync import ( "github.com/MakeNowJust/heredoc/v2" - "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/tfe" + "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/terraform" "github.com/spf13/cobra" ) @@ -13,10 +13,11 @@ func NewSyncCmd() *cobra.Command { Example: heredoc.Doc(` $ ctrlc sync aws-eks $ ctrlc sync google-gke + $ ctrlc sync terraform `), } - cmd.AddCommand(tfe.NewSyncTfeCmd()) + cmd.AddCommand(terraform.NewSyncTerraformCmd()) return cmd } diff --git a/cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go similarity index 54% rename from cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go rename to cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go index de93885..845db7b 100644 --- a/cmd/ctrlc/root/sync/tfe/tfe-org-workspaces.go +++ b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go @@ -1,35 +1,39 @@ -package tfe +package terraform import ( "context" "encoding/json" "fmt" + "net/url" + "time" "strconv" + "github.com/avast/retry-go" "github.com/charmbracelet/log" "github.com/hashicorp/go-tfe" ) const ( - Kind = "Workspace" + Kind = "Workspace" + Version = "terraform/v1" ) type WorkspaceResource struct { - Version string - Kind string - Name string + Config map[string]interface{} Identifier string - Config map[string]string + Kind string Metadata map[string]string + Name string + Version string } -func getLinksMetadata(workspace *tfe.Workspace, baseURL string) *string { +func getLinksMetadata(workspace *tfe.Workspace, baseURL url.URL) *string { if workspace.Organization == nil { return nil } links := map[string]string{ - "Terraform Workspace": fmt.Sprintf("%s/app/%s/workspaces/%s", baseURL, workspace.Organization.Name, workspace.Name), + "Terraform Workspace": fmt.Sprintf("%s/app/%s/workspaces/%s", baseURL.String(), workspace.Organization.Name, workspace.Name), } linksJSON, err := json.Marshal(links) if err != nil { @@ -70,15 +74,15 @@ func getWorkspaceTags(workspace *tfe.Workspace) map[string]string { return tags } -func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL string) (WorkspaceResource, error) { +func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL url.URL) (WorkspaceResource, error) { if workspace == nil { return WorkspaceResource{}, fmt.Errorf("workspace is nil") } - version := workspace.TerraformVersion + version := Version kind := Kind name := workspace.Name identifier := workspace.ID - config := map[string]string{ + config := map[string]interface{}{ "workspaceId": workspace.ID, } metadata := map[string]string{ @@ -94,19 +98,16 @@ func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL string) (Works metadata["ctrlplane/links"] = *linksMetadata } - variables := getWorkspaceVariables(workspace) - for key, value := range variables { - metadata[key] = value - } - - tags := getWorkspaceTags(workspace) - for key, value := range tags { - metadata[key] = value + moreValues := []map[string]string{ + getWorkspaceVariables(workspace), + getWorkspaceTags(workspace), + getWorkspaceVcsRepo(workspace), } - vcsRepo := getWorkspaceVcsRepo(workspace) - for key, value := range vcsRepo { - metadata[key] = value + for _, moreValue := range moreValues { + for key, value := range moreValue { + metadata[key] = value + } } return WorkspaceResource{ @@ -119,16 +120,60 @@ func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL string) (Works }, nil } -func getWorkspacesInOrg(client *tfe.Client, organization string) ([]*tfe.Workspace, error) { +func listWorkspacesWithRetry(ctx context.Context, client *tfe.Client, organization string, pageNum, pageSize int) (*tfe.WorkspaceList, error) { + var workspaces *tfe.WorkspaceList + err := retry.Do( + func() error { + var err error + workspaces, err = client.Workspaces.List(ctx, organization, &tfe.WorkspaceListOptions{ + ListOptions: tfe.ListOptions{ + PageNumber: pageNum, + PageSize: pageSize, + }, + }) + return err + }, + retry.Attempts(5), + retry.Delay(time.Second), + retry.MaxDelay(5*time.Second), + ) + return workspaces, err +} - // TODO: use cmd context - ctx := context.Background() +func listAllWorkspaces(ctx context.Context, client *tfe.Client, organization string) ([]*tfe.Workspace, error) { + var allWorkspaces []*tfe.Workspace + pageNum := 1 + pageSize := 100 - items, err := client.Workspaces.List(ctx, organization, &tfe.WorkspaceListOptions{}) + for { + workspaces, err := listWorkspacesWithRetry(ctx, client, organization, pageNum, pageSize) + if err != nil { + return nil, fmt.Errorf("failed to list workspaces: %w", err) + } + + allWorkspaces = append(allWorkspaces, workspaces.Items...) + if len(workspaces.Items) < pageSize { + break + } + pageNum++ + } + + return allWorkspaces, nil +} + +func getWorkspacesInOrg(ctx context.Context, client *tfe.Client, organization string) ([]WorkspaceResource, error) { + workspaces, err := listAllWorkspaces(ctx, client, organization) if err != nil { return nil, err } - workspaces := items.Items - return workspaces, nil + workspaceResources := []WorkspaceResource{} + for _, workspace := range workspaces { + workspaceResource, err := convertWorkspaceToResource(workspace, client.BaseURL()) + if err != nil { + return nil, err + } + workspaceResources = append(workspaceResources, workspaceResource) + } + return workspaceResources, nil } diff --git a/cmd/ctrlc/root/sync/terraform/terraform.go b/cmd/ctrlc/root/sync/terraform/terraform.go new file mode 100644 index 0000000..e6df9ef --- /dev/null +++ b/cmd/ctrlc/root/sync/terraform/terraform.go @@ -0,0 +1,111 @@ +package terraform + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/ctrlplanedev/cli/internal/api" + "github.com/ctrlplanedev/cli/internal/cliutil" + "github.com/google/uuid" + "github.com/hashicorp/go-tfe" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewSyncTerraformCmd() *cobra.Command { + var workspaceId string + var organization string + + cmd := &cobra.Command{ + Use: "terraform", + Short: "Sync Terraform resources into Ctrlplane", + Example: heredoc.Doc(` + # To set the Terraform token, add TFE_TOKEN to your environment variables. + export TFE_TOKEN=... + + # To set the Terraform address, add TFE_ADDRESS to your environment variables. + export TFE_ADDRESS=... else the default address (https://app.terraform.io) is used. + + # Sync all workspaces in an organization + $ ctrlc sync terraform --organization my-org workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 + `), + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Syncing Terraform resources into Ctrlplane") + apiURL := viper.GetString("url") + apiKey := viper.GetString("api-key") + ctx := cmd.Context() + if _, err := uuid.Parse(workspaceId); err != nil { + return fmt.Errorf("invalid workspace ID: %w", err) + } + + ctrlplaneClient, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey) + if err != nil { + return fmt.Errorf("failed to create API client: %w", err) + } + + terraformClient, err := tfe.NewClient(tfe.DefaultConfig()) + if err != nil { + return fmt.Errorf("failed to create Terraform client: %w", err) + } + + providerName := fmt.Sprintf("tf-%s", organization) + resp, err := ctrlplaneClient.UpsertResourceProviderWithResponse(ctx, workspaceId, providerName) + if err != nil { + return fmt.Errorf("failed to upsert resource provider: %w", err) + } + + if resp.JSON200 == nil { + return fmt.Errorf("failed to upsert resource provider: %s", resp.Body) + } + + providerId := resp.JSON200.Id + fmt.Println("Provider ID:", providerId) + workspaces, err := getWorkspacesInOrg(cmd.Context(), terraformClient, organization) + if err != nil { + return fmt.Errorf("failed to get workspaces in organization: %w", err) + } + + resources := []struct { + Config map[string]interface{} `json:"config"` + Identifier string `json:"identifier"` + Kind string `json:"kind"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Version string `json:"version"` + }{} + + for _, workspace := range workspaces { + resource := struct { + Config map[string]interface{} `json:"config"` + Identifier string `json:"identifier"` + Kind string `json:"kind"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Version string `json:"version"` + }{ + Version: workspace.Version, + Identifier: workspace.Identifier, + Metadata: workspace.Metadata, + Name: workspace.Name, + Kind: workspace.Kind, + Config: workspace.Config, + } + resources = append(resources, resource) + } + + upsertResp, err := ctrlplaneClient.SetResourceProvidersResources(ctx, providerId, api.SetResourceProvidersResourcesJSONRequestBody{ + Resources: resources, + }) + if err != nil { + return fmt.Errorf("failed to upsert resources: %w", err) + } + + return cliutil.HandleOutput(cmd, upsertResp) + }, + } + + cmd.Flags().StringVarP(&organization, "organization", "o", "", "Terraform organization name") + cmd.Flags().StringVarP(&workspaceId, "workspace-id", "w", "", "Ctrlplane workspace ID") + + return cmd +} diff --git a/cmd/ctrlc/root/sync/tfe/tfe.go b/cmd/ctrlc/root/sync/tfe/tfe.go deleted file mode 100644 index 1757b22..0000000 --- a/cmd/ctrlc/root/sync/tfe/tfe.go +++ /dev/null @@ -1,61 +0,0 @@ -package tfe - -import ( - "fmt" - - "github.com/MakeNowJust/heredoc/v2" - "github.com/ctrlplanedev/cli/internal/api" - "github.com/hashicorp/go-tfe" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func validateArgs(organization, workspace string) error { - if organization == "" && workspace == "" { - return fmt.Errorf("either organization or workspace must be provided") - } - if organization != "" && workspace != "" { - return fmt.Errorf("only one of organization or workspace can be provided") - } - return nil -} - -func NewSyncTfeCmd() *cobra.Command { - var ctrlplaneWorkspaceId string - var organization string - var workspace string - - cmd := &cobra.Command{ - Use: "tfe", - Short: "Sync TFE resources into Ctrlplane", - Example: heredoc.Doc(` - # Sync all workspaces in an organization - $ ctrlc sync tfe --organization my-org --ctrlplane-workspace-id 1234567890 - - # Sync a specific workspace - $ ctrlc sync tfe --workspace 1234567890 --ctrlplane-workspace-id 1234567890 - `), - RunE: func(cmd *cobra.Command, args []string) error { - apiURL := viper.GetString("url") - apiKey := viper.GetString("api-key") - - client, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey) - if err != nil { - return fmt.Errorf("failed to create API client: %w", err) - } - - if err := validateArgs(organization, workspace); err != nil { - return fmt.Errorf("invalid arguments: %w", err) - } - - tfeClient, err := tfe.NewClient(tfe.DefaultConfig()) - if err != nil { - return fmt.Errorf("failed to create TFE client: %w", err) - } - - return nil - }, - } - - return cmd -} diff --git a/go.mod b/go.mod index cd35418..768d0c1 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v0.10.0 // indirect github.com/charmbracelet/log v0.4.0 // indirect From 0bd0639ebeb4589e71af8212568b3d803a2b500f Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 09:47:49 -0800 Subject: [PATCH 03/10] rabbit --- cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go | 5 ++++- cmd/ctrlc/root/sync/terraform/terraform.go | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go index 845db7b..ce1ef0e 100644 --- a/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go +++ b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go @@ -87,12 +87,15 @@ func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL url.URL) (Work } metadata := map[string]string{ "ctrlplane/external-id": workspace.ID, - "terraform-cloud/organization": workspace.Organization.Name, "terraform-cloud/workspace-name": workspace.Name, "terraform-cloud/workspace-auto-apply": strconv.FormatBool(workspace.AutoApply), "terraform/version": workspace.TerraformVersion, } + if workspace.Organization != nil { + metadata["terraform-cloud/organization"] = workspace.Organization.Name + } + linksMetadata := getLinksMetadata(workspace, baseURL) if linksMetadata != nil { metadata["ctrlplane/links"] = *linksMetadata diff --git a/cmd/ctrlc/root/sync/terraform/terraform.go b/cmd/ctrlc/root/sync/terraform/terraform.go index e6df9ef..127fc0d 100644 --- a/cmd/ctrlc/root/sync/terraform/terraform.go +++ b/cmd/ctrlc/root/sync/terraform/terraform.go @@ -27,13 +27,18 @@ func NewSyncTerraformCmd() *cobra.Command { export TFE_ADDRESS=... else the default address (https://app.terraform.io) is used. # Sync all workspaces in an organization - $ ctrlc sync terraform --organization my-org workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 + $ ctrlc sync terraform --organization my-org --workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 `), RunE: func(cmd *cobra.Command, args []string) error { fmt.Println("Syncing Terraform resources into Ctrlplane") apiURL := viper.GetString("url") apiKey := viper.GetString("api-key") ctx := cmd.Context() + + if organization == "" { + return fmt.Errorf("organization is required") + } + if _, err := uuid.Parse(workspaceId); err != nil { return fmt.Errorf("invalid workspace ID: %w", err) } From a60fcf935985c51387f288ec481ea077e7ba08f2 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 11:37:26 -0800 Subject: [PATCH 04/10] cli image --- .github/workflows/release.yaml | 67 ++++++++++++++++++++++++++++++++++ docker/Dockerfile | 14 +++++++ docker/README.md | 39 ++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/README.md diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a7bfd1f..68bec46 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,3 +30,70 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + + docker: + runs-on: ubuntu-latest + + if: github.repository_owner == 'ctrlplanedev' + + permissions: + contents: read + id-token: write + + strategy: + matrix: + platform: [linux/amd64] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Check if Docker Hub secrets are available + run: | + if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then + echo "DOCKERHUB_LOGIN=false" >> $GITHUB_ENV + else + echo "DOCKERHUB_LOGIN=true" >> $GITHUB_ENV + fi + + - name: Login to Docker Hub + uses: docker/login-action@v3 + if: env.DOCKERHUB_LOGIN == 'true' + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ctrlplane/cli + tags: | + type=raw,value=latest + type=ref,event=tag + + - name: Build + uses: docker/build-push-action@v6 + if: github.ref != 'refs/heads/main' + with: + push: false + file: docker/Dockerfile + platforms: ${{ matrix.platform }} + tags: ${{ steps.meta.outputs.tags }} + + - name: Build and Push + uses: docker/build-push-action@v6 + if: github.ref == 'refs/heads/main' && env.DOCKERHUB_LOGIN == 'true' + with: + push: true + file: docker/Dockerfile + platforms: ${{ matrix.platform }} + tags: ${{ steps.meta.outputs.tags }} + build-args: | + CLI_VERSION=${{ github.ref_name }} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..453ef8e --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:3.19 + +ARG CLI_VERSION + +RUN apk add --no-cache curl tar && \ + curl -L --fail "https://github.com/ctrlplanedev/cli/releases/download/${CLI_VERSION}/ctrlc_Linux_x86_64.tar.gz" -o ctrlc.tar.gz && \ + tar xzf ctrlc.tar.gz && \ + mv ctrlc /usr/local/bin/ && \ + chmod +x /usr/local/bin/ctrlc && \ + rm -rf ctrlc.tar.gz + +WORKDIR /app + +CMD ["ctrlc", "--help"] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..ceb8e60 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,39 @@ +# Ctrlplane CLI Docker Image + +Official Docker image for the Ctrlplane CLI. + +# Usage + +## Pull the image + +```sh +docker pull ctrlplane/cli:latest +``` + +or pull a specific version: + +```sh +docker pull ctrlplane/cli:v0.1.0 +``` + +## Run the image + +```sh +docker run ctrlplane/cli ctrlc [your-command] +``` + +### Required environment variables + +- `CTRLPLANE_API_KEY`: Your Ctrlplane API key. +- `CTRLPLANE_URL`: The URL of your Ctrlplane instance (e.g. `https://app.ctrlplane.dev`). + +### Terraform sync + +In order to sync Terraform resources into Ctrlplane, you need to set the following environment variables: + +- `TFE_TOKEN`: Your Terraform Cloud API token. +- `TFE_ADDRESS` (optional): The URL of your Terraform Cloud instance (e.g. `https://app.terraform.io`). If not set, the default address (`https://app.terraform.io`) is used. + +```sh +docker run ctrlplane/cli ctrlc sync terraform --organization my-org --workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 +``` From bdf8e63250c2a06b0770a4e8145844c36f88cd1f Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 11:40:31 -0800 Subject: [PATCH 05/10] cleanup --- .github/workflows/release.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 68bec46..459ac5c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -78,15 +78,6 @@ jobs: type=raw,value=latest type=ref,event=tag - - name: Build - uses: docker/build-push-action@v6 - if: github.ref != 'refs/heads/main' - with: - push: false - file: docker/Dockerfile - platforms: ${{ matrix.platform }} - tags: ${{ steps.meta.outputs.tags }} - - name: Build and Push uses: docker/build-push-action@v6 if: github.ref == 'refs/heads/main' && env.DOCKERHUB_LOGIN == 'true' From 0e50bd3a8f406669e79fee42a0487e09756e40b2 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 11:44:39 -0800 Subject: [PATCH 06/10] more cleanup --- .../root/sync/terraform/terraform-org-workspaces.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go index ce1ef0e..dca1900 100644 --- a/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go +++ b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go @@ -68,8 +68,10 @@ func getWorkspaceVcsRepo(workspace *tfe.Workspace) map[string]string { func getWorkspaceTags(workspace *tfe.Workspace) map[string]string { tags := make(map[string]string) for _, tag := range workspace.Tags { - key := fmt.Sprintf("terraform-cloud/tag/%s", tag.Name) - tags[key] = "true" + if tag != nil { + key := fmt.Sprintf("terraform-cloud/tag/%s", tag.Name) + tags[key] = "true" + } } return tags } @@ -174,7 +176,8 @@ func getWorkspacesInOrg(ctx context.Context, client *tfe.Client, organization st for _, workspace := range workspaces { workspaceResource, err := convertWorkspaceToResource(workspace, client.BaseURL()) if err != nil { - return nil, err + log.Error("Failed to convert workspace to resource", "error", err, "workspace", workspace.Name) + continue } workspaceResources = append(workspaceResources, workspaceResource) } From 85dbe7a6ee8589590670ae6307aba7224c1a7526 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 11:55:34 -0800 Subject: [PATCH 07/10] more rabbit comments --- .github/workflows/release.yaml | 2 +- docker/Dockerfile | 2 -- docker/README.md | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 459ac5c..9800d0b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -80,7 +80,7 @@ jobs: - name: Build and Push uses: docker/build-push-action@v6 - if: github.ref == 'refs/heads/main' && env.DOCKERHUB_LOGIN == 'true' + if: env.DOCKERHUB_LOGIN == 'true' with: push: true file: docker/Dockerfile diff --git a/docker/Dockerfile b/docker/Dockerfile index 453ef8e..e69c653 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,6 +9,4 @@ RUN apk add --no-cache curl tar && \ chmod +x /usr/local/bin/ctrlc && \ rm -rf ctrlc.tar.gz -WORKDIR /app - CMD ["ctrlc", "--help"] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index ceb8e60..1f8220f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -35,5 +35,6 @@ In order to sync Terraform resources into Ctrlplane, you need to set the followi - `TFE_ADDRESS` (optional): The URL of your Terraform Cloud instance (e.g. `https://app.terraform.io`). If not set, the default address (`https://app.terraform.io`) is used. ```sh -docker run ctrlplane/cli ctrlc sync terraform --organization my-org --workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 +docker run -e TFE_TOKEN=my-token -e CTRLPLANE_API_KEY=my-api-key -e CTRLPLANE_URL=https://app.ctrlplane.dev \ + ctrlplane/cli ctrlc sync terraform --organization my-org --workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 ``` From 3c26ab19ce9de8172302d182f8e1075b4b2f4992 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 13:07:32 -0800 Subject: [PATCH 08/10] cleanup --- .github/workflows/release.yaml | 17 +++++++++++++++-- docker/Dockerfile | 11 +++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9800d0b..661e310 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,9 +9,11 @@ permissions: on: push: - # run only against tags + # run only against tags on the main branch tags: - "v*" + branches: + - main jobs: linux: @@ -78,6 +80,17 @@ jobs: type=raw,value=latest type=ref,event=tag + - name: Build Only + uses: docker/build-push-action@v6 + if: env.DOCKERHUB_LOGIN != 'true' + with: + push: false + file: docker/Dockerfile + platforms: ${{ matrix.platform }} + tags: ${{ steps.meta.outputs.tags }} + build-args: | + CLI_VERSION=${{ steps.meta.outputs.version }} + - name: Build and Push uses: docker/build-push-action@v6 if: env.DOCKERHUB_LOGIN == 'true' @@ -87,4 +100,4 @@ jobs: platforms: ${{ matrix.platform }} tags: ${{ steps.meta.outputs.tags }} build-args: | - CLI_VERSION=${{ github.ref_name }} + CLI_VERSION=${{ steps.meta.outputs.version }} diff --git a/docker/Dockerfile b/docker/Dockerfile index e69c653..6b6e25c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.19 +FROM alpine:3.19 AS builder ARG CLI_VERSION @@ -6,7 +6,10 @@ RUN apk add --no-cache curl tar && \ curl -L --fail "https://github.com/ctrlplanedev/cli/releases/download/${CLI_VERSION}/ctrlc_Linux_x86_64.tar.gz" -o ctrlc.tar.gz && \ tar xzf ctrlc.tar.gz && \ mv ctrlc /usr/local/bin/ && \ - chmod +x /usr/local/bin/ctrlc && \ - rm -rf ctrlc.tar.gz + chmod +x /usr/local/bin/ctrlc -CMD ["ctrlc", "--help"] \ No newline at end of file +FROM alpine:3.19 AS final + +COPY --from=builder /usr/local/bin/ctrlc /usr/local/bin/ctrlc + +CMD ["ctrlc", "--help"] From 1e7c8b2b56a3bb12b26772ed282986b692e6e9e8 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 13:14:37 -0800 Subject: [PATCH 09/10] nit --- .github/workflows/release.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 661e310..e74923e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,11 +9,9 @@ permissions: on: push: - # run only against tags on the main branch + # run only against tags tags: - "v*" - branches: - - main jobs: linux: From 4450cb28d8215421683f690932ca5274da52f036 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Jan 2025 13:21:20 -0800 Subject: [PATCH 10/10] foix --- .github/workflows/release.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e74923e..ff3bfd7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -71,12 +71,14 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ctrlplane/cli tags: | type=raw,value=latest type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} - name: Build Only uses: docker/build-push-action@v6