Skip to content

Commit fd803c8

Browse files
authored
IAC-3333: use workspace yaml file (#83)
1 parent 6b008bc commit fd803c8

File tree

3 files changed

+214
-42
lines changed

3 files changed

+214
-42
lines changed

iacm-plan.go

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"os"
1111
"os/signal"
1212
"path/filepath"
13+
"strings"
1314
"time"
1415

1516
"github.com/fatih/color"
1617
"github.com/hashicorp/go-slug"
18+
"gopkg.in/yaml.v3"
1719

1820
"github.com/urfave/cli/v2"
1921
)
@@ -28,7 +30,10 @@ const (
2830
folderPathWarningMsg = "The workspace is configured with the folder path %s,\nHarness will upload the following directory and its contents: \n%s"
2931
noFolderPathWarningMsg = "The workspace has no configured folder path,\nHarness will upload the following directory and its contents \n%s"
3032
folderPathNotFoundErr = "The folder path configured in the workspace %s does not exist in the current directory"
31-
folderPathErr = "An error occurred when trying to find the folder path in the current directory: %v"
33+
folderPathErr = "An error occurred when trying to find the repo root from the current current directory: %v"
34+
35+
workspaceInfoCliArgErr = "When supplying workspace info via CLI arguments the org-id, project-id and workspace-id must all be present"
36+
workspaceInfoFileErr = "No workspace.yaml file present in the .harness folder in the current directory, consider creating one or supplying workspace info via cli argument"
3237

3338
startingStepMsg = "========================== Starting step %s ==========================\n"
3439
startingStageMsg = "========================== Starting stage %s ==========================\n"
@@ -48,6 +53,12 @@ type LogClient interface {
4853
Blob(ctx context.Context, key string) error
4954
}
5055

56+
type WorkspaceInfo struct {
57+
Org string `yaml:"org"`
58+
Project string `yaml:"project"`
59+
Workspace string `yaml:"workspace"`
60+
}
61+
5162
type IacmCommand struct {
5263
client IacmClient
5364
logClient LogClient
@@ -69,8 +80,20 @@ func NewIacmCommand(account string, client IacmClient, logClient LogClient) Iacm
6980
}
7081

7182
func (c IacmCommand) executePlan(ctx context.Context) error {
72-
fmt.Printf("Fetching workspace %s... \n", c.workspace)
73-
ws, err := c.client.GetWorkspace(ctx, c.org, c.project, c.workspace)
83+
wd, err := os.Getwd()
84+
if err != nil {
85+
fmt.Println(utils.GetColoredText(err.Error(), color.FgRed))
86+
return err
87+
}
88+
89+
workspaceInfo, err := getWorkspaceInfo(c.org, c.project, c.workspace, wd)
90+
if err != nil {
91+
fmt.Println(utils.GetColoredText(err.Error(), color.FgRed))
92+
return err
93+
}
94+
95+
fmt.Printf("Fetching workspace %s... \n", workspaceInfo.Workspace)
96+
ws, err := c.client.GetWorkspace(ctx, workspaceInfo.Org, workspaceInfo.Project, workspaceInfo.Workspace)
7497
if err != nil {
7598
fmt.Printf("An error occurred when fetching the workspace: %v \n", err)
7699
return err
@@ -86,12 +109,7 @@ func (c IacmCommand) executePlan(ctx context.Context) error {
86109
utils.GetColoredText(defaultPipeline, color.FgCyan),
87110
)
88111

89-
wd, err := os.Getwd()
90-
if err != nil {
91-
return err
92-
}
93-
94-
warning, err := validateWorkingDirectory(wd, ws)
112+
repoRoot, warning, err := getRepoRootFromWorkingDirectory(wd, ws)
95113
if err != nil {
96114
fmt.Println(utils.GetColoredText(err.Error(), color.FgRed))
97115
return err
@@ -110,7 +128,7 @@ func (c IacmCommand) executePlan(ctx context.Context) error {
110128
}
111129

112130
archive := bytes.NewBuffer([]byte{})
113-
_, err = packer.Pack(wd, archive)
131+
_, err = packer.Pack(repoRoot, archive)
114132
if err != nil {
115133
return err
116134
}
@@ -122,26 +140,26 @@ func (c IacmCommand) executePlan(ctx context.Context) error {
122140
if len(c.targets) > 0 {
123141
customArguments["target"] = c.targets
124142
}
125-
plan, err := c.client.CreateRemoteExecution(ctx, c.org, c.project, c.workspace, customArguments)
143+
plan, err := c.client.CreateRemoteExecution(ctx, ws.Org, ws.Project, ws.Identifier, customArguments)
126144
if err != nil {
127145
fmt.Println(utils.GetColoredText(fmt.Sprintf("An error occurred creating the remote execution: %v", err.Error()), color.FgRed))
128146
return err
129147
}
130148

131-
plan, err = c.client.UploadRemoteExecution(ctx, c.org, c.project, c.workspace, plan.ID, archive.Bytes())
149+
plan, err = c.client.UploadRemoteExecution(ctx, ws.Org, ws.Project, ws.Identifier, plan.ID, archive.Bytes())
132150
if err != nil {
133151
fmt.Println(utils.GetColoredText(fmt.Sprintf("An error occurred uploading the source code: %v", err.Error()), color.FgRed))
134152
return err
135153
}
136154

137-
plan, err = c.client.ExecuteRemoteExecution(ctx, c.org, c.project, c.workspace, plan.ID)
155+
plan, err = c.client.ExecuteRemoteExecution(ctx, ws.Org, ws.Project, ws.Identifier, plan.ID)
138156
if err != nil {
139157
fmt.Println(utils.GetColoredText(fmt.Sprintf("An error occurred executing the pipeline: %v", err.Error()), color.FgRed))
140158
return err
141159
}
142160
fmt.Printf("Pipeline execution: %s\n", utils.GetColoredText(plan.PipelineExecutionURL, color.FgCyan))
143161

144-
startingNodeID, err := c.getStartingNodeID(ctx, c.org, c.project, plan.PipelineExecutionID)
162+
startingNodeID, err := c.getStartingNodeID(ctx, ws.Org, ws.Project, plan.PipelineExecutionID)
145163
if err != nil {
146164
fmt.Println(utils.GetColoredText(fmt.Sprintf("An error occurred fetching starting node id: %v", err.Error()), color.FgRed))
147165
return err
@@ -202,22 +220,79 @@ func getDefaultPipeline(defaultPipelines map[string]*client.DefaultPipelineOverr
202220
return "", err
203221
}
204222

205-
func validateWorkingDirectory(workingDirectory string, workspace *client.Workspace) (string, error) {
206-
if workspace.RepositoryPath != "" {
207-
_, err := os.Stat(filepath.Join(workingDirectory, workspace.RepositoryPath))
223+
func getRepoRootFromWorkingDirectory(workingDirectory string, workspace *client.Workspace) (string, string, error) {
224+
if workspace.RepositoryPath == "" {
225+
return workingDirectory, fmt.Sprintf(noFolderPathWarningMsg, utils.GetColoredText(workingDirectory, color.FgCyan)), nil
226+
}
227+
228+
workingDirectory = filepath.Clean(workingDirectory)
229+
repositoryPath := filepath.Clean(workspace.RepositoryPath)
230+
// if the working directory is the same as the configured workspace repository
231+
// path we trim the repository path from the working directory to find the repo
232+
// root
233+
if strings.HasSuffix(workingDirectory, repositoryPath) {
234+
repoRoot := strings.TrimSuffix(workingDirectory, workspace.RepositoryPath)
235+
repoRoot = filepath.Clean(repoRoot)
236+
_, err := os.Stat(repoRoot)
208237
if err != nil {
209-
if os.IsNotExist(err) {
210-
return "", fmt.Errorf(folderPathNotFoundErr, workspace.RepositoryPath)
211-
}
212-
return "", fmt.Errorf(folderPathErr, err)
238+
return "", "", fmt.Errorf(folderPathErr, err)
213239
}
214-
return fmt.Sprintf(
240+
return repoRoot,
241+
fmt.Sprintf(
242+
folderPathWarningMsg,
243+
utils.GetColoredText(repositoryPath, color.FgCyan),
244+
utils.GetColoredText(repoRoot, color.FgCyan),
245+
), nil
246+
}
247+
248+
// the working directory is not the repository path so we try and find
249+
// the repository path within the working directory and if found use the
250+
// working directory as the repo root
251+
_, err := os.Stat(filepath.Join(workingDirectory, repositoryPath))
252+
if os.IsNotExist(err) {
253+
return "", "", fmt.Errorf(folderPathNotFoundErr, repositoryPath)
254+
}
255+
256+
return workingDirectory,
257+
fmt.Sprintf(
215258
folderPathWarningMsg,
216-
utils.GetColoredText(workspace.RepositoryPath, color.FgCyan),
259+
utils.GetColoredText(repositoryPath, color.FgCyan),
217260
utils.GetColoredText(workingDirectory, color.FgCyan),
218261
), nil
262+
}
263+
264+
// getWorkspaceInfo returns the values supplied directly to the cli if they are present
265+
// and falls back to a config file in .harness/workspace.yaml if not
266+
func getWorkspaceInfo(org, project, workspace, workingDirectory string) (*WorkspaceInfo, error) {
267+
if org != "" && project != "" && workspace != "" {
268+
return &WorkspaceInfo{
269+
Org: org,
270+
Project: project,
271+
Workspace: workspace,
272+
}, nil
273+
}
274+
if org == "" && (project != "" || workspace != "") {
275+
return nil, errors.New(workspaceInfoCliArgErr)
276+
}
277+
if project == "" && (org != "" || workspace != "") {
278+
return nil, errors.New(workspaceInfoCliArgErr)
279+
}
280+
if workspace == "" && (org != "" || project != "") {
281+
return nil, errors.New(workspaceInfoCliArgErr)
282+
}
283+
284+
file, err := os.ReadFile(filepath.Join(workingDirectory, ".harness/workspace.yaml"))
285+
if err != nil {
286+
if os.IsNotExist(err) {
287+
return nil, errors.New(workspaceInfoFileErr)
288+
}
289+
}
290+
workspaceInfo := &WorkspaceInfo{}
291+
err = yaml.Unmarshal(file, workspaceInfo)
292+
if err != nil {
293+
return nil, err
219294
}
220-
return fmt.Sprintf(noFolderPathWarningMsg, utils.GetColoredText(workingDirectory, color.FgCyan)), nil
295+
return workspaceInfo, nil
221296
}
222297

223298
func (c *IacmCommand) getStartingNodeID(ctx context.Context, org, project, executionID string) (string, error) {

iacm-plan_test.go

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111

1212
"github.com/stretchr/testify/assert"
13+
"gopkg.in/yaml.v3"
1314
)
1415

1516
type GetPipelineExecutionMock struct {
@@ -37,35 +38,41 @@ func (m *GetPipelineExecutionMock) GetLogToken(ctx context.Context) (string, err
3738
}
3839

3940
func TestValidateWorkingDirectory(t *testing.T) {
40-
t.Run("working directory does not contain folder path", func(*testing.T) {
41+
t.Run("working directory does not contain repository path", func(*testing.T) {
4142
ws := &client.Workspace{
4243
RepositoryPath: "tf/aws",
4344
}
44-
wd := t.TempDir()
45-
warning, err := validateWorkingDirectory(wd, ws)
45+
actualRepoRoot := t.TempDir()
46+
workingDirectory := actualRepoRoot
47+
repoRoot, warning, err := getRepoRootFromWorkingDirectory(workingDirectory, ws)
48+
assert.Empty(t, repoRoot)
4649
assert.Empty(t, warning)
4750
assert.Equal(t, fmt.Sprintf(folderPathNotFoundErr, ws.RepositoryPath), err.Error())
4851
})
49-
t.Run("working directory contains folder path", func(*testing.T) {
52+
t.Run("working directory contains the repository path", func(*testing.T) {
5053
ws := &client.Workspace{
51-
RepositoryPath: "tf/aws",
54+
RepositoryPath: "tf/a3ws",
5255
}
53-
wd := t.TempDir()
54-
err := os.MkdirAll(filepath.Join(wd, ws.RepositoryPath), 0700)
56+
actualRepoRoot := t.TempDir()
57+
workingDirectory := actualRepoRoot
58+
err := os.MkdirAll(filepath.Join(actualRepoRoot, ws.RepositoryPath), 0700)
5559
assert.Nil(t, err)
56-
warning, err := validateWorkingDirectory(wd, ws)
57-
assert.Equal(t, fmt.Sprintf(folderPathWarningMsg, ws.RepositoryPath, wd), warning)
60+
repoRoot, warning, err := getRepoRootFromWorkingDirectory(workingDirectory, ws)
61+
assert.Equal(t, repoRoot, actualRepoRoot)
62+
assert.Equal(t, fmt.Sprintf(folderPathWarningMsg, ws.RepositoryPath, actualRepoRoot), warning)
5863
assert.Nil(t, err)
5964
})
60-
t.Run("no folder path", func(*testing.T) {
65+
t.Run("working directory is the same as the repository path", func(*testing.T) {
6166
ws := &client.Workspace{
62-
RepositoryPath: "",
67+
RepositoryPath: "tf/aws",
6368
}
64-
wd := t.TempDir()
65-
err := os.MkdirAll(filepath.Join(wd, ws.RepositoryPath), 0700)
69+
actualRepoRoot := t.TempDir()
70+
workingDirectory := filepath.Join(actualRepoRoot, ws.RepositoryPath)
71+
err := os.MkdirAll(workingDirectory, 0700)
6672
assert.Nil(t, err)
67-
warning, err := validateWorkingDirectory(wd, ws)
68-
assert.Equal(t, fmt.Sprintf(noFolderPathWarningMsg, wd), warning)
73+
repoRoot, warning, err := getRepoRootFromWorkingDirectory(workingDirectory, ws)
74+
assert.Equal(t, repoRoot, actualRepoRoot)
75+
assert.Equal(t, fmt.Sprintf(folderPathWarningMsg, ws.RepositoryPath, actualRepoRoot), warning)
6976
assert.Nil(t, err)
7077
})
7178
}
@@ -291,3 +298,93 @@ func TestGetStartingNodeID(t *testing.T) {
291298
assert.Equal(t, expectedErr, actualErr)
292299
})
293300
}
301+
302+
func TestWorkspaceInfo(t *testing.T) {
303+
tt := map[string]struct {
304+
ExpectedWorkspaceInfo *WorkspaceInfo
305+
ExpectedErr error
306+
CliArgWorkspaceInfo *WorkspaceInfo
307+
FileWorkspaceInfo *WorkspaceInfo
308+
}{
309+
"cli args are returned when they are all present": {
310+
ExpectedWorkspaceInfo: &WorkspaceInfo{
311+
Org: "cli-arg-org",
312+
Project: "cli-arg-project",
313+
Workspace: "cli-arg-workspace",
314+
},
315+
ExpectedErr: nil,
316+
CliArgWorkspaceInfo: &WorkspaceInfo{
317+
Org: "cli-arg-org",
318+
Project: "cli-arg-project",
319+
Workspace: "cli-arg-workspace",
320+
},
321+
FileWorkspaceInfo: &WorkspaceInfo{
322+
Org: "file-org",
323+
Project: "file-project",
324+
Workspace: "file-workspace",
325+
},
326+
},
327+
"an error is returned cli arguments are not all present": {
328+
ExpectedWorkspaceInfo: nil,
329+
ExpectedErr: errors.New(workspaceInfoCliArgErr),
330+
CliArgWorkspaceInfo: &WorkspaceInfo{
331+
Org: "cli-arg-org",
332+
Project: "cli-arg-project",
333+
},
334+
FileWorkspaceInfo: &WorkspaceInfo{
335+
Org: "file-org",
336+
Project: "file-project",
337+
Workspace: "file-workspace",
338+
},
339+
},
340+
"file info is returned when file is present": {
341+
ExpectedWorkspaceInfo: &WorkspaceInfo{
342+
Org: "file-org",
343+
Project: "file-project",
344+
Workspace: "file-workspace",
345+
},
346+
ExpectedErr: nil,
347+
CliArgWorkspaceInfo: &WorkspaceInfo{
348+
Org: "",
349+
Project: "",
350+
Workspace: "",
351+
},
352+
FileWorkspaceInfo: &WorkspaceInfo{
353+
Org: "file-org",
354+
Project: "file-project",
355+
Workspace: "file-workspace",
356+
},
357+
},
358+
"an error is returned when file is not present": {
359+
ExpectedWorkspaceInfo: nil,
360+
ExpectedErr: errors.New(workspaceInfoFileErr),
361+
CliArgWorkspaceInfo: &WorkspaceInfo{
362+
Org: "",
363+
Project: "",
364+
Workspace: "",
365+
},
366+
FileWorkspaceInfo: nil,
367+
},
368+
}
369+
for name, test := range tt {
370+
t.Run(name, func(t *testing.T) {
371+
dir := t.TempDir()
372+
if test.FileWorkspaceInfo != nil {
373+
workspaceFileYaml, err := yaml.Marshal(test.FileWorkspaceInfo)
374+
assert.Nil(t, err)
375+
err = os.Mkdir(filepath.Join(dir, ".harness/"), 0777)
376+
assert.Nil(t, err)
377+
err = os.WriteFile(filepath.Join(dir, ".harness/workspace.yaml"), workspaceFileYaml, 0777)
378+
assert.Nil(t, err)
379+
}
380+
actualWorkspaceInfo, actualErr := getWorkspaceInfo(
381+
test.CliArgWorkspaceInfo.Org,
382+
test.CliArgWorkspaceInfo.Project,
383+
test.CliArgWorkspaceInfo.Workspace,
384+
dir,
385+
)
386+
assert.Equal(t, test.ExpectedErr, actualErr)
387+
assert.Equal(t, test.ExpectedWorkspaceInfo, actualWorkspaceInfo)
388+
})
389+
}
390+
}

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -625,17 +625,17 @@ func main() {
625625
&cli.StringFlag{
626626
Name: "workspace-id",
627627
Usage: "provide a workspace id",
628-
Required: true,
628+
Required: false,
629629
},
630630
altsrc.NewStringFlag(&cli.StringFlag{
631631
Name: "org-id",
632632
Usage: "provide an Organization Identifier",
633-
Required: true,
633+
Required: false,
634634
}),
635635
altsrc.NewStringFlag(&cli.StringFlag{
636636
Name: "project-id",
637637
Usage: "provide a Project Identifier",
638-
Required: true,
638+
Required: false,
639639
}),
640640
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
641641
Name: "target",

0 commit comments

Comments
 (0)