diff --git a/internal/infrastructure/octopus.go b/internal/infrastructure/octopus.go new file mode 100644 index 0000000..a80963d --- /dev/null +++ b/internal/infrastructure/octopus.go @@ -0,0 +1,294 @@ +package infrastructure + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tasks" + "github.com/mcasperson/OctoterraWizard/internal/octoclient" + "github.com/mcasperson/OctoterraWizard/internal/state" + "github.com/samber/lo" + "io" + "net/http" + "strings" + "time" +) + +func WaitForTask(state state.State, taskId string) error { + myclient, err := octoclient.CreateClient(state) + + if err != nil { + return err + } + + // wait up to 10 minutes for the task to complete + for i := 0; i < 600; i++ { + mytasks, err := myclient.Tasks.Get(tasks.TasksQuery{ + Environment: "", + HasPendingInterruptions: false, + HasWarningsOrErrors: false, + IDs: []string{taskId}, + IncludeSystem: false, + IsActive: false, + IsRunning: false, + Name: "", + Node: "", + PartialName: "", + Project: "", + Runbook: "", + Skip: 0, + Spaces: nil, + States: nil, + Take: 1, + Tenant: "", + }) + + if err != nil { + return err + } + + if mytasks.Items[0].IsCompleted != nil && *mytasks.Items[0].IsCompleted { + if mytasks.Items[0].State != "Success" { + return errors.New("task was not successful") + } + return nil + } + + fmt.Println(mytasks.Items[0].State) + + time.Sleep(10 * time.Second) + } + + return errors.New("task did not complete in 10 minutes") +} + +func RunRunbook(state state.State, runbookName string, projectName string) (string, error) { + myclient, err := octoclient.CreateClient(state) + + if err != nil { + return "", err + } + + environment, err := environments.GetAll(myclient, myclient.GetSpaceID()) + + if err != nil { + return "", err + } + + project, err := projects.GetByName(myclient, myclient.GetSpaceID(), projectName) + + if err != nil { + return "", err + } + + runbook, err := runbooks.GetByName(myclient, myclient.GetSpaceID(), project.GetID(), runbookName) + + if err != nil { + return "", err + } + + url := state.Server + runbook.GetLinks()["RunbookRunPreview"] + url = strings.ReplaceAll(url, "{environment}", environment[0].GetID()) + url = strings.ReplaceAll(url, "{?includeDisabledSteps}", "") + + runbookRunPreviewRequest, err := http.NewRequest("GET", url, nil) + + if err != nil { + return "", err + } + + runbookRunPreviewResponse, err := myclient.HttpSession().DoRawRequest(runbookRunPreviewRequest) + + if err != nil { + return "", err + } + + runbookRunPreviewRaw, err := io.ReadAll(runbookRunPreviewResponse.Body) + + if err != nil { + return "", err + } + + runbookRunPreview := map[string]any{} + err = json.Unmarshal(runbookRunPreviewRaw, &runbookRunPreview) + + if err != nil { + return "", err + } + + runbookFormNames := lo.Map(runbookRunPreview["Form"].(map[string]any)["Elements"].([]any), func(value any, index int) any { + return value.(map[string]any)["Name"] + }) + + runbookFormValues := map[string]string{} + + for _, name := range runbookFormNames { + runbookFormValues[name.(string)] = "dummy" + } + + runbookBody := map[string]any{ + "RunbookId": runbook.GetID(), + "RunbookSnapShotId": runbook.PublishedRunbookSnapshotID, + "FrozenRunbookProcessId": nil, + "EnvironmentId": environment[0].GetID(), + "TenantId": nil, + "SkipActions": []string{}, + "QueueTime": nil, + "QueueTimeExpiry": nil, + "FormValues": runbookFormValues, + "ForcePackageDownload": false, + "ForcePackageRedeployment": true, + "UseGuidedFailure": false, + "SpecificMachineIds": []string{}, + "ExcludedMachineIds": []string{}, + } + + runbookBodyJson, err := json.Marshal(runbookBody) + + if err != nil { + return "", err + } + + url = state.Server + "/api/" + state.Space + "/runbookRuns" + runbookRunRequest, err := http.NewRequest("POST", url, bytes.NewReader(runbookBodyJson)) + + if err != nil { + return "", err + } + + runbookRunResponse, err := myclient.HttpSession().DoRawRequest(runbookRunRequest) + + if err != nil { + return "", err + } + + runbookRunRaw, err := io.ReadAll(runbookRunResponse.Body) + + if err != nil { + return "", err + } + + runbookRun := map[string]any{} + err = json.Unmarshal(runbookRunRaw, &runbookRun) + + if err != nil { + return "", err + } + + return runbookRun["TaskId"].(string), nil + +} + +func PublishRunbook(state state.State, runbookName string, projectName string) error { + myclient, err := octoclient.CreateClient(state) + + if err != nil { + return err + } + + project, err := projects.GetByName(myclient, myclient.GetSpaceID(), projectName) + + if err != nil { + return err + } + + runbook, err := runbooks.GetByName(myclient, myclient.GetSpaceID(), project.GetID(), runbookName) + + if err != nil { + return err + } + + url := state.Server + runbook.GetLinks()["RunbookSnapshotTemplate"] + runbookSnapshotTemplateRequest, err := http.NewRequest("GET", url, nil) + + if err != nil { + return err + } + + runbookSnapshotTemplateResponse, err := myclient.HttpSession().DoRawRequest(runbookSnapshotTemplateRequest) + + if err != nil { + return err + } + + runbookSnapshotTemplateRaw, err := io.ReadAll(runbookSnapshotTemplateResponse.Body) + + if err != nil { + return err + } + + runbookSnapshotTemplate := map[string]any{} + err = json.Unmarshal(runbookSnapshotTemplateRaw, &runbookSnapshotTemplate) + + if err != nil { + return err + } + + snapshot := map[string]any{ + "ProjectId": project.GetID(), + "RunbookId": runbook.GetID(), + "Name": runbookSnapshotTemplate["NextNameIncrement"], + } + + var packageErrors error = nil + snapshot["SelectedPackages"] = lo.Map(runbookSnapshotTemplate["Packages"].([]any), func(pkg any, index int) any { + snapshotPackage := pkg.(map[string]any) + versions, err := feeds.SearchPackageVersions(myclient, myclient.GetSpaceID(), snapshotPackage["FeedId"].(string), snapshotPackage["PackageId"].(string), "", 1) + + if err != nil { + packageErrors = errors.Join(packageErrors, err) + return nil + } + + return map[string]any{ + "ActionName": snapshotPackage["ActionName"], + "Version": versions.Items[0].Version, + "PackageReferenceName": snapshotPackage["PackageReferenceName"], + } + }) + + if packageErrors != nil { + return packageErrors + } + + snapshotJson, err := json.Marshal(snapshot) + + if err != nil { + return err + } + + url = state.Server + "/api/" + state.Space + "/runbookSnapshots?publish=true" + runbookSnapshotRequest, err := http.NewRequest("POST", url+"?publish=true", bytes.NewBuffer(snapshotJson)) + + if err != nil { + return err + } + + runbookSnapshotResponse, err := myclient.HttpSession().DoRawRequest(runbookSnapshotRequest) + + if err != nil { + return err + } + + runbookSnapshotResponseRaw, err := io.ReadAll(runbookSnapshotResponse.Body) + + if err != nil { + return err + } + + runbookSnapshot := map[string]any{} + err = json.Unmarshal(runbookSnapshotResponseRaw, &runbookSnapshot) + + if err != nil { + return err + } + + fmt.Println(runbookSnapshot) + + return nil +} diff --git a/internal/steps/project_export.go b/internal/steps/project_export.go index ca82ca6..dbf5004 100644 --- a/internal/steps/project_export.go +++ b/internal/steps/project_export.go @@ -45,7 +45,7 @@ func (s ProjectExportStep) GetContainer(parent fyne.Window) *fyne.Container { Wizard: s.Wizard, BaseStep: BaseStep{State: s.State}}) }, func() { - s.Wizard.ShowWizardStep(FinishStep{ + s.Wizard.ShowWizardStep(StartSpaceExportStep{ Wizard: s.Wizard, BaseStep: BaseStep{State: s.State}}) }) diff --git a/internal/steps/start_space_export.go b/internal/steps/start_space_export.go new file mode 100644 index 0000000..c797917 --- /dev/null +++ b/internal/steps/start_space_export.go @@ -0,0 +1,83 @@ +package steps + +import ( + "fmt" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" + "github.com/mcasperson/OctoterraWizard/internal/infrastructure" + "github.com/mcasperson/OctoterraWizard/internal/strutil" + "github.com/mcasperson/OctoterraWizard/internal/wizard" +) + +type StartSpaceExportStep struct { + BaseStep + Wizard wizard.Wizard + exportSpace *widget.Button +} + +func (s StartSpaceExportStep) GetContainer(parent fyne.Window) *fyne.Container { + + bottom, previous, next := s.BuildNavigation(func() { + s.Wizard.ShowWizardStep(ProjectExportStep{ + Wizard: s.Wizard, + BaseStep: BaseStep{State: s.State}}) + }, func() { + s.Wizard.ShowWizardStep(FinishStep{ + Wizard: s.Wizard, + BaseStep: BaseStep{State: s.State}}) + }) + next.Disable() + + label1 := widget.NewLabel(strutil.TrimMultilineWhitespace(` + The source space is now ready to begin exporting to the destination space. + We start by serializing the space level resources (feeds, accounts, tenants, certificates, targets etc) using the runbooks in the "Octoterra Space Management" project. + First, we run the "__ 1. Serialize Space" runbook to create the Terraform module. + Then we run the "__ 2. Deploy Space" runbook to apply the Terraform module to the destination space. + Click the "Export Space" button to execute these runbooks. + `)) + result := widget.NewLabel("") + infinite := widget.NewProgressBarInfinite() + infinite.Hide() + infinite.Start() + s.exportSpace = widget.NewButton("Export Space", func() { + s.exportSpace.Disable() + next.Disable() + previous.Disable() + infinite.Show() + defer s.exportSpace.Enable() + defer previous.Enable() + defer infinite.Hide() + + result.SetText("🔵 Running the runbooks.") + + if err := s.Execute(); err != nil { + result.SetText(fmt.Sprintf("🔴 Failed to publish and run the runbooks: %s", err)) + } else { + result.SetText("🟢 Runbooks ran successfully.") + next.Enable() + } + }) + middle := container.New(layout.NewVBoxLayout(), label1, s.exportSpace, infinite, result) + + content := container.NewBorder(nil, bottom, nil, nil, middle) + + return content +} + +func (s StartSpaceExportStep) Execute() (executeError error) { + if err := infrastructure.PublishRunbook(s.State, "__ 1. Serialize Space", "Octoterra Space Management"); err != nil { + return err + } + + if taskId, err := infrastructure.RunRunbook(s.State, "__ 1. Serialize Space", "Octoterra Space Management"); err != nil { + return err + } else { + if err := infrastructure.WaitForTask(s.State, taskId); err != nil { + return err + } + } + + return nil +} diff --git a/internal/steps/start_space_export_test.go b/internal/steps/start_space_export_test.go new file mode 100644 index 0000000..04f4593 --- /dev/null +++ b/internal/steps/start_space_export_test.go @@ -0,0 +1,44 @@ +package steps + +import ( + "fyne.io/fyne/v2/test" + "github.com/mcasperson/OctoterraWizard/internal/state" + "github.com/mcasperson/OctoterraWizard/internal/wizard" + "testing" +) + +func TestStartSpaceExportStep_GetContainer(t *testing.T) { + return + + testWindow := test.NewWindow(nil) + defer testWindow.Close() + + step := StartSpaceExportStep{ + Wizard: wizard.Wizard{}, + BaseStep: BaseStep{State: state.State{ + BackendType: "", + Server: "http://172.17.0.1:8080/", + ApiKey: "API-AAAAA", + Space: "Spaces-1", + DestinationServer: "", + DestinationApiKey: "", + DestinationSpace: "", + AwsAccessKey: "", + AwsSecretKey: "", + AwsS3Bucket: "", + AwsS3BucketRegion: "", + PromptForDelete: false, + AzureResourceGroupName: "", + AzureStorageAccountName: "", + AzureContainerName: "", + AzureSubscriptionId: "", + AzureTenantId: "", + AzureApplicationId: "", + AzurePassword: "", + }}, + } + + if err := step.Execute(); err != nil { + t.Error(err) + } +}