diff --git a/README.md b/README.md index b746d6c..9283493 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,24 @@ Global Flags: -r, --read-only-mode read-only mode (e.g. "-r") ``` +### Workspace Consumers Help +```text +$ tfc-ops workspaces consumers -h +Add to workspace remote state consumers. (Possible future capability: list, replace, delete) + +Usage: + tfc-ops workspaces consumers [flags] + +Flags: + --consumers string required - List of remote state consumer workspaces, comma-separated + -h, --help help for consumers + -w, --workspace string required - Partial workspace name to search across all workspaces + +Global Flags: + -o, --organization string required - Name of Terraform Cloud Organization + -r, --read-only-mode read-only mode (e.g. "-r") +``` + ### Workspace List Help Any workspace attribute that can be read by the Terraform API can be retrieved @@ -285,6 +303,23 @@ Global Flags: -r, --read-only-mode read-only mode (e.g. "-r") ``` +### Variable Sets List Help +```text +List variable sets applied to a workspace + +Usage: + tfc-ops varsets list [flags] + +Flags: + -h, --help help for list + -w, --workspace string Name of the Workspace in Terraform Cloud + --workspace-filter string Partial workspace name to search across all workspaces + +Global Flags: + -o, --organization string required - Name of Terraform Cloud Organization + -r, --read-only-mode read-only mode (e.g. "-r") +``` + ## License tfc-ops is released under the Apache 2.0 license. See [LICENSE](https://github.com/silinternational/tfc-ops/blob/main/LICENSE) diff --git a/cmd/root.go b/cmd/root.go index 5a3af6b..cf75c68 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) const requiredPrefix = "required - " diff --git a/cmd/root_test.go b/cmd/root_test.go index ecffda0..73cae1a 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,7 +1,6 @@ package cmd import ( - "reflect" "testing" ) @@ -33,12 +32,11 @@ func Test_stringMapToSlice(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1 := stringMapToSlice(tt.input) - if !reflect.DeepEqual(got, tt.wantKeys) { - t.Errorf("stringMapToSlice() got = %v, want %v", got, tt.wantKeys) - } - if !reflect.DeepEqual(got1, tt.wantValues) { - t.Errorf("stringMapToSlice() got1 = %v, want %v", got1, tt.wantValues) + keys, values := stringMapToSlice(tt.input) + for i := range keys { + if tt.input[keys[i]] != values[i] { + t.Errorf("stringMapToSlice() got = %v, want %v", values[i], tt.input[keys[i]]) + } } }) } diff --git a/cmd/variablesAdd.go b/cmd/variablesAdd.go index cc5c182..02548f6 100644 --- a/cmd/variablesAdd.go +++ b/cmd/variablesAdd.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) var variablesAddCmd = &cobra.Command{ diff --git a/cmd/variablesDelete.go b/cmd/variablesDelete.go index 948170d..d418b08 100644 --- a/cmd/variablesDelete.go +++ b/cmd/variablesDelete.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) var key string diff --git a/cmd/variablesList.go b/cmd/variablesList.go index 2528b35..00f6c6d 100644 --- a/cmd/variablesList.go +++ b/cmd/variablesList.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" - api "github.com/silinternational/tfc-ops/lib" + api "github.com/silinternational/tfc-ops/v3/lib" ) var ( diff --git a/cmd/variablesUpdate.go b/cmd/variablesUpdate.go index 4e2d827..78ac511 100644 --- a/cmd/variablesUpdate.go +++ b/cmd/variablesUpdate.go @@ -8,7 +8,7 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) var ( diff --git a/cmd/varsetsApply.go b/cmd/varsetsApply.go index b0f44df..5ff278e 100644 --- a/cmd/varsetsApply.go +++ b/cmd/varsetsApply.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) var varsetsApplyCmd = &cobra.Command{ diff --git a/cmd/varsetsList.go b/cmd/varsetsList.go index 0f5cf3d..a68ab31 100644 --- a/cmd/varsetsList.go +++ b/cmd/varsetsList.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) var varsetsListCmd = &cobra.Command{ diff --git a/cmd/workspaces.go b/cmd/workspaces.go index e10ad1e..d2d259f 100644 --- a/cmd/workspaces.go +++ b/cmd/workspaces.go @@ -29,4 +29,5 @@ var workspaceCmd = &cobra.Command{ func init() { rootCmd.AddCommand(workspaceCmd) addGlobalFlags(workspaceCmd) + addConsumersCommand(workspaceCmd) } diff --git a/cmd/workspacesClone.go b/cmd/workspacesClone.go index f854ac3..5657145 100644 --- a/cmd/workspacesClone.go +++ b/cmd/workspacesClone.go @@ -20,7 +20,7 @@ import ( "github.com/spf13/cobra" - cloner "github.com/silinternational/tfc-ops/lib" + cloner "github.com/silinternational/tfc-ops/v3/lib" ) var ( diff --git a/cmd/workspacesConsumers.go b/cmd/workspacesConsumers.go new file mode 100644 index 0000000..b1bd852 --- /dev/null +++ b/cmd/workspacesConsumers.go @@ -0,0 +1,82 @@ +// Copyright © 2023 SIL International +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "log" + "strings" + + "github.com/spf13/cobra" + + "github.com/silinternational/tfc-ops/v3/lib" +) + +const ( + flagConsumers = "consumers" + flagWorkspace = "workspace" +) + +func addConsumersCommand(parentCommand *cobra.Command) { + var consumers string + workspaceConsumersCmd := &cobra.Command{ + Use: flagConsumers, + Short: "Manage workspace remote state consumers", + Long: `Add to workspace remote state consumers. (Possible future capability: list, replace, delete)`, + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + runWorkspaceConsumers(consumers) + }, + } + + parentCommand.AddCommand(workspaceConsumersCmd) + + workspaceConsumersCmd.Flags().StringVarP(&workspace, flagWorkspace, "w", "", + requiredPrefix+"Partial workspace name to search across all workspaces") + + workspaceConsumersCmd.Flags().StringVar(&consumers, flagConsumers, "", + requiredPrefix+"List of remote state consumer workspaces, comma-separated") + + requiredFlags := []string{flagConsumers, flagWorkspace} + for _, flag := range requiredFlags { + if err := workspaceConsumersCmd.MarkFlagRequired(flag); err != nil { + panic("MarkFlagRequired failed with error: " + err.Error()) + } + } +} + +func runWorkspaceConsumers(consumers string) { + workspaceData, err := lib.GetWorkspaceData(organization, workspace) + if err != nil { + log.Fatalln("workspace consumers", err) + } + + consumersList := strings.Split(consumers, ",") + consumerIDs := make([]string, len(consumersList)) + for i, consumer := range consumersList { + consumerData, err := lib.GetWorkspaceData(organization, consumer) + if err != nil { + log.Fatalln("workspace consumers", err) + } + consumerIDs[i] = consumerData.Data.ID + } + + fmt.Printf("Adding to %s: %s", workspace, consumers) + if !readOnlyMode { + if err := lib.AddRemoteStateConsumers(workspaceData.Data.ID, consumerIDs); err != nil { + log.Fatalln("workspace consumers", err) + } + } +} diff --git a/cmd/workspacesList.go b/cmd/workspacesList.go index 5a6e380..315ad04 100644 --- a/cmd/workspacesList.go +++ b/cmd/workspacesList.go @@ -20,7 +20,7 @@ import ( "github.com/spf13/cobra" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) var attributes string diff --git a/cmd/workspacesUpdate.go b/cmd/workspacesUpdate.go index 4bd2b4c..838c9b3 100644 --- a/cmd/workspacesUpdate.go +++ b/cmd/workspacesUpdate.go @@ -20,7 +20,7 @@ import ( "github.com/spf13/cobra" - "github.com/silinternational/tfc-ops/lib" + "github.com/silinternational/tfc-ops/v3/lib" ) const ( diff --git a/go.mod b/go.mod index dbb44a3..06fd216 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/Jeffail/gabs/v2 v2.7.0 github.com/manifoldco/promptui v0.9.0 - github.com/silinternational/tfc-ops v0.0.0-20230615063809-ce4eb07ae327 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.15.0 ) @@ -25,6 +24,7 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // 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 284710c..445be9a 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -159,8 +160,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/silinternational/tfc-ops v0.0.0-20230615063809-ce4eb07ae327 h1:FIjLOT+xVLBUFpRoYknW0mRSSX4ceA6aOqqaSVr9fqY= -github.com/silinternational/tfc-ops v0.0.0-20230615063809-ce4eb07ae327/go.mod h1:RMRC4iOQ+/KmPXqbeMOqEKGZQp/15oswaIHVlyoa1kU= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -483,6 +482,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/lib/client.go b/lib/client.go index 427b2d6..6323f66 100644 --- a/lib/client.go +++ b/lib/client.go @@ -350,6 +350,12 @@ func getWorkspacePage(url string) (WorkspaceList, error) { } func GetWorkspaceData(organization, workspaceName string) (WorkspaceJSON, error) { + if organization == "" { + return WorkspaceJSON{}, fmt.Errorf("GetWorkspaceData: organization is required") + } + if workspaceName == "" { + return WorkspaceJSON{}, fmt.Errorf("GetWorkspaceData: workspace is required") + } u := NewTfcUrl(fmt.Sprintf( "/organizations/%s/workspaces/%s", organization, @@ -520,7 +526,7 @@ func AssignTeamAccess(workspaceID string, allTeamData AllTeamWorkspaceData) { teamData.Relationships.Team.Data.ID, ) - resp := callAPI("POST", url, postData, nil) + resp := callAPI(http.MethodPost, url, postData, nil) defer resp.Body.Close() } return @@ -535,7 +541,7 @@ func CreateVariable(organization, workspaceName string, tfVar TFVar) { postData := GetCreateVariablePayload(organization, workspaceName, tfVar) - resp := callAPI("POST", url, postData, nil) + resp := callAPI(http.MethodPost, url, postData, nil) defer resp.Body.Close() // bodyBytes, _ := ioutil.ReadAll(resp.Body) @@ -584,7 +590,7 @@ func UpdateVariable(organization, workspaceName, variableID string, tfVar TFVar) patchData := GetUpdateVariablePayload(organization, workspaceName, variableID, tfVar) - resp := callAPI("PATCH", url, patchData, nil) + resp := callAPI(http.MethodPatch, url, patchData, nil) defer resp.Body.Close() // bodyBytes, _ := ioutil.ReadAll(resp.Body) @@ -602,7 +608,7 @@ func CreateWorkspace(oc OpsConfig, vcsTokenID string) (string, error) { postData := GetCreateWorkspacePayload(oc, vcsTokenID) - resp := callAPI("POST", url, postData, nil) + resp := callAPI(http.MethodPost, url, postData, nil) defer resp.Body.Close() // bodyBytes, _ := ioutil.ReadAll(resp.Body) @@ -626,7 +632,7 @@ func CreateWorkspace2(oc OpsConfig, vcsTokenID string) (Workspace, error) { postData := GetCreateWorkspacePayload(oc, vcsTokenID) - resp := callAPI("POST", url, postData, nil) + resp := callAPI(http.MethodPost, url, postData, nil) defer resp.Body.Close() // bodyBytes, _ := ioutil.ReadAll(resp.Body) @@ -981,7 +987,7 @@ func UpdateWorkspace(params WorkspaceUpdateParams) error { } for id, name := range foundWs { url := fmt.Sprintf(baseURL+"/workspaces/%s", id) - resp := callAPI("PATCH", url, postData, nil) + resp := callAPI(http.MethodPatch, url, postData, nil) bodyBytes, _ := ioutil.ReadAll(resp.Body) _ = resp.Body.Close() @@ -1247,3 +1253,26 @@ func ListWorkspaceVariableSets(workspaceID string) (VariableSetList, error) { return variableSetList, nil } + +func AddRemoteStateConsumers(workspaceID string, consumerIDs []string) error { + u := NewTfcUrl(fmt.Sprintf("/workspaces/%s/relationships/remote-state-consumers", workspaceID)) + + data := gabs.New() + _, err := data.ArrayOfSize(len(consumerIDs), "data") + if err != nil { + return fmt.Errorf("ArrayOfSize failed in AddRemoteStateConsumers: %w", err) + } + for i, id := range consumerIDs { + if _, err := data.S("data").SetIndex(map[string]any{ + "type": "workspaces", + "id": id, + }, i); err != nil { + return fmt.Errorf("SetIndex failed in AddRemoteStateConsumers: %w", err) + } + } + postData := data.String() + + _ = callAPI(http.MethodPost, u.String(), postData, nil) + + return nil +} diff --git a/main.go b/main.go index 9f669c2..2313fb0 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ package main -import "github.com/silinternational/tfc-ops/cmd" +import "github.com/silinternational/tfc-ops/v3/cmd" func main() { cmd.Execute()