Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,74 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

docker:
Copy link
Member

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

Copy link
Member

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

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- 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
- name: Check if Docker Hub secrets are available
run: |
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 }}"
🧰 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 }}
2 changes: 2 additions & 0 deletions cmd/ctrlc/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/agent"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/api"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/config"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -44,6 +45,7 @@ func NewRootCmd() *cobra.Command {
cmd.AddCommand(agent.NewAgentCmd())
cmd.AddCommand(api.NewAPICmd())
cmd.AddCommand(config.NewConfigCmd())
cmd.AddCommand(sync.NewSyncCmd())

return cmd
}
6 changes: 5 additions & 1 deletion cmd/ctrlc/root/sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ package sync

import (
"github.com/MakeNowJust/heredoc/v2"
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/terraform"
"github.com/spf13/cobra"
)

func NewRootCmd() *cobra.Command {
func NewSyncCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "sync <integration>",
Short: "Sync resources into Ctrlplane",
Example: heredoc.Doc(`
$ ctrlc sync aws-eks
$ ctrlc sync google-gke
$ ctrlc sync terraform
`),
}

cmd.AddCommand(terraform.NewSyncTerraformCmd())

return cmd
}
185 changes: 185 additions & 0 deletions cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go
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
}
Loading