Skip to content

Commit

Permalink
Merge pull request #63 from silinternational/develop
Browse files Browse the repository at this point in the history
Release 3.2.0 - clone variable sets
  • Loading branch information
briskt authored Jun 15, 2023
2 parents 7f76c1d + f4aa036 commit ce4eb07
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 25 deletions.
8 changes: 2 additions & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"os"
"strings"

"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand Down Expand Up @@ -84,11 +83,8 @@ func initConfig() {
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
home, err := os.UserHomeDir()
cobra.CheckErr(err)

// Search config in home directory with name ".tfc-ops" (without extension).
viper.AddConfigPath(home)
Expand Down
74 changes: 74 additions & 0 deletions cmd/varsetsList.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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"

"github.com/spf13/cobra"

"github.com/silinternational/tfc-ops/lib"
)

var varsetsListCmd = &cobra.Command{
Use: "list",
Short: "List Variable Sets",
Long: `List variable sets applied to a workspace`,
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
runVarsetsList()
},
}

func init() {
varsetsCmd.AddCommand(varsetsListCmd)

varsetsListCmd.Flags().StringVarP(&workspace, "workspace", "w", "",
"Name of the Workspace in Terraform Cloud")

varsetsListCmd.Flags().StringVar(&workspaceFilter, "workspace-filter", "",
"Partial workspace name to search across all workspaces")
}

func runVarsetsList() {
if workspace == "" && workspaceFilter == "" {
errLog.Fatalln("Either --workspace or --workspace-filter must be specified.")
}

var workspaces map[string]string
if workspace != "" {
w, err := lib.GetWorkspaceByName(organization, workspace)
if err != nil {
errLog.Fatalf("error getting workspace %q from Terraform: %s", workspace, err)
}
workspaces = map[string]string{w.ID: workspace}
} else {
workspaces = lib.FindWorkspaces(organization, workspaceFilter)
if len(workspaces) == 0 {
errLog.Fatalf("no workspaces match the filter '%s'", workspaceFilter)
}
}

for id, name := range workspaces {
sets, err := lib.ListWorkspaceVariableSets(id)
if err != nil {
return
}
fmt.Printf("Workspace %s has the following variable sets:\n", name)
for _, set := range sets.Data {
fmt.Printf(" %s\n", set.Attributes.Name)
}
}
}
14 changes: 12 additions & 2 deletions cmd/workspacesClone.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
var (
copyState bool
copyVariables bool
applyVariableSets bool
differentDestinationAccount bool
newOrganization string
sourceWorkspace string
Expand Down Expand Up @@ -60,6 +61,7 @@ var cloneCmd = &cobra.Command{
NewVCSTokenID: newVCSTokenID,
CopyState: copyState,
CopyVariables: copyVariables,
ApplyVariableSets: applyVariableSets,
DifferentDestinationAccount: differentDestinationAccount,
}

Expand Down Expand Up @@ -111,6 +113,12 @@ func init() {
false,
`optional (e.g. "-c=true") whether to copy the values of the Source Workspace variables.`,
)
cloneCmd.Flags().BoolVar(
&applyVariableSets,
"applyVariableSets",
false,
`optional, whether to apply the same variable sets to the new workspace (only for same-account clone).`,
)
cloneCmd.Flags().BoolVarP(
&differentDestinationAccount,
"differentDestinationAccount",
Expand All @@ -137,8 +145,10 @@ func runClone(cfg cloner.CloneConfig) {
fmt.Print("Info: ATLAS_TOKEN_DESTINATION is not set, using ATLAS_TOKEN for destination account.\n\n")
}

fmt.Printf("clone called using %s, %s, %s, copyState: %t, copyVariables: %t, differentDestinationAccount: %t\n",
cfg.Organization, cfg.SourceWorkspace, cfg.NewWorkspace, cfg.CopyState, cfg.CopyVariables, cfg.DifferentDestinationAccount)
fmt.Printf("clone called using %s, %s, %s, copyState: %t, copyVariables: %t, "+
"applyVariableSets: %t, differentDestinationAccount: %t\n",
cfg.Organization, cfg.SourceWorkspace, cfg.NewWorkspace, cfg.CopyState, cfg.CopyVariables,
cfg.ApplyVariableSets, cfg.DifferentDestinationAccount)

sensitiveVars, err := cloner.CloneWorkspace(cfg)
if err != nil {
Expand Down
112 changes: 95 additions & 17 deletions lib/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type CloneConfig struct {
AtlasTokenDestination string
CopyState bool
CopyVariables bool
ApplyVariableSets bool
DifferentDestinationAccount bool
}

Expand Down Expand Up @@ -244,7 +245,7 @@ type WorkspaceUpdateParams struct {
}

// ConvertHCLVariable changes a TFVar struct in place by escaping
// the double quotes and line endings in the Value attribute
// the double quotes and line endings in the Value attribute
func ConvertHCLVariable(tfVar *TFVar) {
if !tfVar.Hcl {
return
Expand Down Expand Up @@ -615,12 +616,37 @@ func CreateWorkspace(oc OpsConfig, vcsTokenID string) (string, error) {
return wsData.Data.ID, nil
}

// CreateWorkspace2 makes a Terraform workspaces API call to create a workspace for a given organization, including
// setting up its VCS repo integration. Returns the properties of the new workspace.
func CreateWorkspace2(oc OpsConfig, vcsTokenID string) (Workspace, error) {
url := fmt.Sprintf(
baseURL+"/organizations/%s/workspaces",
oc.NewOrg,
)

postData := GetCreateWorkspacePayload(oc, vcsTokenID)

resp := callAPI("POST", url, postData, nil)

defer resp.Body.Close()
// bodyBytes, _ := ioutil.ReadAll(resp.Body)
// fmt.Println(string(bodyBytes))

var wsData WorkspaceJSON

if err := json.NewDecoder(resp.Body).Decode(&wsData); err != nil {
return Workspace{}, fmt.Errorf("error getting created workspace data: %s\n", err)
}
return wsData.Data, nil
}

// RunTFInit ...
// - removes old terraform.tfstate files
// - runs terraform init with old versions
// - runs terraform init with new version
// - removes old terraform.tfstate files
// - runs terraform init with old versions
// - runs terraform init with new version
//
// NOTE: This procedure can be used to copy/migrate a workspace's state to a new one.
// (see the -backend-config mention below and the backend.tf file in this repo)
// (see the -backend-config mention below and the backend.tf file in this repo)
func RunTFInit(oc OpsConfig, tfToken, tfTokenDestination string) error {
var tfInit string
var err error
Expand Down Expand Up @@ -696,11 +722,12 @@ func RunTFInit(oc OpsConfig, tfToken, tfTokenDestination string) error {
}

// CloneWorkspace gets the data, variables and team access data for an existing Terraform Cloud workspace
// and then creates a clone of it with the same data.
// and then creates a clone of it with the same data.
//
// If the copyVariables param is set to true, then all the non-sensitive variable values will be added to the new
// workspace. Otherwise, they will be set to "REPLACE_THIS_VALUE"
// workspace. Otherwise, they will be set to "REPLACE_THIS_VALUE"
func CloneWorkspace(cfg CloneConfig) ([]string, error) {
wsData, err := GetWorkspaceData(cfg.Organization, cfg.SourceWorkspace)
sourceWsData, err := GetWorkspaceData(cfg.Organization, cfg.SourceWorkspace)
if err != nil {
return []string{}, err
}
Expand All @@ -712,18 +739,18 @@ func CloneWorkspace(cfg CloneConfig) ([]string, error) {

if !cfg.DifferentDestinationAccount {
cfg.NewOrganization = cfg.Organization
cfg.NewVCSTokenID = wsData.Data.Attributes.VCSRepo.Identifier
cfg.NewVCSTokenID = sourceWsData.Data.Attributes.VCSRepo.Identifier
}

oc := OpsConfig{
SourceOrg: cfg.Organization,
SourceName: wsData.Data.Attributes.Name,
SourceName: sourceWsData.Data.Attributes.Name,
NewOrg: cfg.NewOrganization,
NewName: cfg.NewWorkspace,
TerraformVersion: wsData.Data.Attributes.TerraformVersion,
RepoID: wsData.Data.Attributes.VCSRepo.Identifier,
Branch: wsData.Data.Attributes.VCSRepo.Branch,
Directory: wsData.Data.Attributes.WorkingDirectory,
TerraformVersion: sourceWsData.Data.Attributes.TerraformVersion,
RepoID: sourceWsData.Data.Attributes.VCSRepo.Identifier,
Branch: sourceWsData.Data.Attributes.VCSRepo.Branch,
Directory: sourceWsData.Data.Attributes.WorkingDirectory,
}

sensitiveVars := []string{}
Expand Down Expand Up @@ -773,11 +800,20 @@ func CloneWorkspace(cfg CloneConfig) ([]string, error) {
return sensitiveVars, nil
}

_, err = CreateWorkspace(oc, wsData.Data.Attributes.VCSRepo.TokenID)
destWsProps, err := CreateWorkspace2(oc, sourceWsData.Data.Attributes.VCSRepo.TokenID)
if err != nil {
return nil, fmt.Errorf("failed to create new workspace: %w", err)
}

err = copyVariableSetList(sourceWsData.Data.ID, destWsProps.ID)
if err != nil {
return nil, fmt.Errorf("failed to clone variable sets: %w", err)
}

CreateAllVariables(oc.NewOrg, oc.NewName, tfVars)

// Get Team Access Data for source Workspace
allTeamData, err := GetTeamAccessFrom(wsData.Data.ID)
allTeamData, err := GetTeamAccessFrom(sourceWsData.Data.ID)
if err != nil {
return sensitiveVars, err
}
Expand All @@ -795,7 +831,7 @@ func CloneWorkspace(cfg CloneConfig) ([]string, error) {

// AddOrUpdateVariable adds or updates an existing Terraform Cloud workspace variable
// If the copyVariables param is set to true, then all the non-sensitive variable values will be added to the new
// workspace. Otherwise, they will be set to "REPLACE_THIS_VALUE"
// workspace. Otherwise, they will be set to "REPLACE_THIS_VALUE"
func AddOrUpdateVariable(cfg UpdateConfig) (string, error) {
variables, err := GetVarsFromWorkspace(cfg.Organization, cfg.Workspace)
if err != nil {
Expand Down Expand Up @@ -1169,3 +1205,45 @@ func ApplyVariableSet(varsetID string, workspaceIDs []string) error {
// TODO: need to look at response?
return nil
}

func copyVariableSetList(sourceWorkspaceID, destinationWorkspaceID string) error {
sets, err := ListWorkspaceVariableSets(sourceWorkspaceID)
if err != nil {
return fmt.Errorf("copy variable sets: %w", err)
}
if err := ApplyVariableSetsToWorkspace(sets, destinationWorkspaceID); err != nil {
return fmt.Errorf("copy variable sets: %w", err)
}
return nil
}

func ApplyVariableSetsToWorkspace(sets VariableSetList, workspaceID string) error {
var failed []string
var err error
for _, set := range sets.Data {
err = ApplyVariableSet(set.ID, []string{workspaceID})
if err != nil {
failed = append(failed, set.Attributes.Name)
}
}
if len(failed) == len(sets.Data) {
return fmt.Errorf("failed to apply variable sets: %w", err)
}
if len(failed) > 0 {
return fmt.Errorf("failed to apply variable sets %s: %w", strings.Join(failed, ", "), err)
}
return nil
}

func ListWorkspaceVariableSets(workspaceID string) (VariableSetList, error) {
u := NewTfcUrl(fmt.Sprintf("/workspaces/%s/varsets", workspaceID))

resp := callAPI(http.MethodGet, u.String(), "", nil)

var variableSetList VariableSetList
if err := json.NewDecoder(resp.Body).Decode(&variableSetList); err != nil {
return variableSetList, fmt.Errorf("unexpected content retrieving variable set list: %w", err)
}

return variableSetList, nil
}

0 comments on commit ce4eb07

Please sign in to comment.