-
Notifications
You must be signed in to change notification settings - Fork 1
fix: Tfe sync command #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
22614a1
771f299
0bd0639
a60fcf9
bdf8e63
0e50bd3
85dbe7a
3c26ab1
1e7c8b2
4450cb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -30,3 +30,74 @@ 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 | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix shell script quoting in Docker Hub secrets check. The shell script has potential word splitting issues. Apply this diff to fix the shell script: - name: Check if Docker Hub secrets are available
run: |
- if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then
+ if [ -z "${DOCKERHUB_USERNAME}" ] || [ -z "${DOCKERHUB_TOKEN}" ]; then
echo "DOCKERHUB_LOGIN=false" >> $GITHUB_ENV
else
echo "DOCKERHUB_LOGIN=true" >> $GITHUB_ENV
fi
+ env:
+ DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
+ DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"📝 Committable suggestion
Suggested change
🧰 Tools🪛 actionlint (1.7.4)60-60: shellcheck reported issue in this script: SC2086:info:2:35: Double quote to prevent globbing and word splitting (shellcheck) 60-60: shellcheck reported issue in this script: SC2086:info:4:34: Double quote to prevent globbing and word splitting (shellcheck) |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - 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@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 | ||||||||||||||||||||||||||||||||||||
| 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' | ||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||
| push: true | ||||||||||||||||||||||||||||||||||||
| file: docker/Dockerfile | ||||||||||||||||||||||||||||||||||||
| platforms: ${{ matrix.platform }} | ||||||||||||||||||||||||||||||||||||
| tags: ${{ steps.meta.outputs.tags }} | ||||||||||||||||||||||||||||||||||||
| build-args: | | ||||||||||||||||||||||||||||||||||||
| CLI_VERSION=${{ steps.meta.outputs.version }} | ||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| 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" | ||
| Version = "terraform/v1" | ||
| ) | ||
|
|
||
| type WorkspaceResource struct { | ||
| Config map[string]interface{} | ||
| Identifier string | ||
| Kind string | ||
| Metadata map[string]string | ||
| Name string | ||
| Version 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.String(), 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 { | ||
| if tag != nil { | ||
| key := fmt.Sprintf("terraform-cloud/tag/%s", tag.Name) | ||
| tags[key] = "true" | ||
| } | ||
| } | ||
| return tags | ||
| } | ||
|
|
||
| func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL url.URL) (WorkspaceResource, error) { | ||
| if workspace == nil { | ||
| return WorkspaceResource{}, fmt.Errorf("workspace is nil") | ||
| } | ||
| version := Version | ||
| kind := Kind | ||
| name := workspace.Name | ||
| identifier := workspace.ID | ||
| config := map[string]interface{}{ | ||
| "workspaceId": workspace.ID, | ||
| } | ||
| metadata := map[string]string{ | ||
| "ctrlplane/external-id": workspace.ID, | ||
| "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 | ||
| } | ||
|
|
||
| moreValues := []map[string]string{ | ||
| getWorkspaceVariables(workspace), | ||
| getWorkspaceTags(workspace), | ||
| getWorkspaceVcsRepo(workspace), | ||
| } | ||
|
|
||
| for _, moreValue := range moreValues { | ||
| for key, value := range moreValue { | ||
| metadata[key] = value | ||
| } | ||
| } | ||
|
|
||
| return WorkspaceResource{ | ||
| Version: version, | ||
| Kind: kind, | ||
| Name: name, | ||
| Identifier: identifier, | ||
| Config: config, | ||
| Metadata: metadata, | ||
| }, nil | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| func listAllWorkspaces(ctx context.Context, client *tfe.Client, organization string) ([]*tfe.Workspace, error) { | ||
| var allWorkspaces []*tfe.Workspace | ||
| pageNum := 1 | ||
| pageSize := 100 | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| workspaceResources := []WorkspaceResource{} | ||
| for _, workspace := range workspaces { | ||
| workspaceResource, err := convertWorkspaceToResource(workspace, client.BaseURL()) | ||
| if err != nil { | ||
| log.Error("Failed to convert workspace to resource", "error", err, "workspace", workspace.Name) | ||
| continue | ||
| } | ||
| workspaceResources = append(workspaceResources, workspaceResource) | ||
| } | ||
| return workspaceResources, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should only do this on main
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do the push on main, not the build, we can do that on every branch