Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.
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
45 changes: 45 additions & 0 deletions .github/workflows/labctl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: labctl

on:
push:
branches: [master]
paths:
- 'tools/labctl/**'
pull_request:
paths:
- 'tools/labctl/**'

jobs:
lint-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: tools/labctl

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: tools/labctl/go.mod
cache-dependency-path: tools/labctl/go.sum

- name: Format check
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "Files not formatted:"
gofmt -l .
exit 1
fi

- name: Vet
run: go vet ./...

- name: Lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.6.0
working-directory: tools/labctl

- name: Test
run: go test -race ./...
1 change: 1 addition & 0 deletions tools/labctl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/
73 changes: 73 additions & 0 deletions tools/labctl/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
run:
timeout: 5m

version: "2"

linters:
default: none
enable:
- govet
- staticcheck
- revive
- errcheck
- ineffassign
- unused
- gosec
- gocritic
- misspell
- sqlclosecheck
- bidichk

settings:
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- whyNoLint
- wrapperFunc
- hugeParam
- rangeValCopy

revive:
rules:
- name: exported
- name: unused-parameter
- name: unreachable-code
- name: redefines-builtin-id
- name: defer
- name: context-as-argument
- name: context-keys-type
- name: error-return
- name: error-naming
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-receiver
- name: get-return
- name: string-of-int
- name: early-return
- name: unconditional-recursion
- name: identical-branches
- name: waitgroup-by-value
- name: atomic

exclusions:
presets: []

issues:
max-issues-per-linter: 0
max-same-issues: 0
30 changes: 30 additions & 0 deletions tools/labctl/cmd/images/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package images

import (
"fmt"

"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
Use: "list",
Short: "List images stored in e2",
Long: "List all images currently stored in the e2 bucket with their metadata.",
RunE: runList,
}

var (
listCredentials string
listSOPSAgeKeyFile string
)

func init() {
listCmd.Flags().StringVar(&listCredentials, "credentials", "", "Path to SOPS-encrypted credentials file")
listCmd.Flags().StringVar(&listSOPSAgeKeyFile, "sops-age-key-file", "", "Path to age private key")
}

func runList(_ *cobra.Command, _ []string) error {
// TODO(HOM-20): Implement list command
fmt.Println("list command not yet implemented")
return nil
}
38 changes: 38 additions & 0 deletions tools/labctl/cmd/images/prune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package images

import (
"fmt"

"github.com/spf13/cobra"
)

var pruneCmd = &cobra.Command{
Use: "prune",
Short: "Remove orphaned images from e2",
Long: `Remove images from e2 that are not in the manifest.

The prune command compares images in e2 storage against the manifest and
removes any that are no longer referenced. This is a manual-only operation
and is not run automatically.`,
RunE: runPrune,
}

var (
pruneManifest string
pruneCredentials string
pruneSOPSAgeKeyFile string
pruneDryRun bool
)

func init() {
pruneCmd.Flags().StringVar(&pruneManifest, "manifest", "./images/images.yaml", "Path to images.yaml")
pruneCmd.Flags().StringVar(&pruneCredentials, "credentials", "", "Path to SOPS-encrypted credentials file")
pruneCmd.Flags().StringVar(&pruneSOPSAgeKeyFile, "sops-age-key-file", "", "Path to age private key")
pruneCmd.Flags().BoolVar(&pruneDryRun, "dry-run", false, "Show what would be removed")
}

func runPrune(_ *cobra.Command, _ []string) error {
// TODO(HOM-20): Implement prune command
fmt.Println("prune command not yet implemented")
return nil
}
21 changes: 21 additions & 0 deletions tools/labctl/cmd/images/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Package images provides CLI commands for managing lab images.
package images

import (
"github.com/spf13/cobra"
)

// Cmd is the images subcommand.
var Cmd = &cobra.Command{
Use: "images",
Short: "Manage lab images",
Long: "Commands for syncing, validating, listing, pruning, and uploading lab images to e2 storage.",
}

func init() {
Cmd.AddCommand(syncCmd)
Cmd.AddCommand(validateCmd)
Cmd.AddCommand(listCmd)
Cmd.AddCommand(pruneCmd)
Cmd.AddCommand(uploadCmd)
}
40 changes: 40 additions & 0 deletions tools/labctl/cmd/images/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package images

import (
"fmt"

"github.com/spf13/cobra"
)

var syncCmd = &cobra.Command{
Use: "sync",
Short: "Sync images to e2 storage",
Long: `Download source images, upload to e2, update files, and create PR if needed.

The sync command reads the image manifest, downloads any new or updated images,
uploads them to e2 storage, and optionally updates file references to trigger
downstream builds.`,
RunE: runSync,
}

var (
syncManifest string
syncCredentials string
syncSOPSAgeKeyFile string
syncDryRun bool
syncForce bool
)

func init() {
syncCmd.Flags().StringVar(&syncManifest, "manifest", "./images/images.yaml", "Path to images.yaml")
syncCmd.Flags().StringVar(&syncCredentials, "credentials", "", "Path to SOPS-encrypted credentials file")
syncCmd.Flags().StringVar(&syncSOPSAgeKeyFile, "sops-age-key-file", "", "Path to age private key for SOPS decryption")
syncCmd.Flags().BoolVar(&syncDryRun, "dry-run", false, "Show what would be done without executing")
syncCmd.Flags().BoolVar(&syncForce, "force", false, "Force re-upload even if checksums match")
}

func runSync(_ *cobra.Command, _ []string) error {
// TODO(HOM-20): Implement sync command
fmt.Println("sync command not yet implemented")
return nil
}
43 changes: 43 additions & 0 deletions tools/labctl/cmd/images/upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package images

import (
"fmt"

"github.com/spf13/cobra"
)

var uploadCmd = &cobra.Command{
Use: "upload",
Short: "Upload a local file to e2",
Long: `Upload a local file to e2 storage.

The upload command is used by Packer workflows to upload built images.
It computes the SHA256 checksum and writes metadata JSON in the same
format as the sync command.`,
RunE: runUpload,
}

var (
uploadSource string
uploadDestination string
uploadCredentials string
uploadSOPSAgeKeyFile string
uploadName string
)

func init() {
uploadCmd.Flags().StringVar(&uploadSource, "source", "", "Path to local file to upload (required)")
uploadCmd.Flags().StringVar(&uploadDestination, "destination", "", "Destination path in e2 bucket (required)")
uploadCmd.Flags().StringVar(&uploadCredentials, "credentials", "", "Path to SOPS-encrypted credentials file")
uploadCmd.Flags().StringVar(&uploadSOPSAgeKeyFile, "sops-age-key-file", "", "Path to age private key")
uploadCmd.Flags().StringVar(&uploadName, "name", "", "Image name for metadata (defaults to destination filename)")

_ = uploadCmd.MarkFlagRequired("source")
_ = uploadCmd.MarkFlagRequired("destination")
}

func runUpload(_ *cobra.Command, _ []string) error {
// TODO(HOM-20): Implement upload command
fmt.Println("upload command not yet implemented")
return nil
}
30 changes: 30 additions & 0 deletions tools/labctl/cmd/images/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package images

import (
"fmt"

"github.com/spf13/cobra"
)

var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate the image manifest",
Long: `Validate manifest syntax, check source URLs, and verify updateFile regex patterns.

The validate command performs a dry-run validation of the image manifest,
checking that all URLs are reachable (via HEAD requests) and that regex
patterns in updateFile sections compile successfully.`,
RunE: runValidate,
}

var validateManifest string

func init() {
validateCmd.Flags().StringVar(&validateManifest, "manifest", "./images/images.yaml", "Path to images.yaml")
}

func runValidate(_ *cobra.Command, _ []string) error {
// TODO(HOM-20): Implement validate command
fmt.Println("validate command not yet implemented")
return nil
}
23 changes: 23 additions & 0 deletions tools/labctl/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Package cmd provides the CLI commands for labctl.
package cmd

import (
"github.com/spf13/cobra"

"github.com/GilmanLab/lab/tools/labctl/cmd/images"
)

var rootCmd = &cobra.Command{
Use: "labctl",
Short: "Lab control CLI for managing infrastructure",
Long: "labctl is a CLI tool for managing lab infrastructure including images, configurations, and deployments.",
}

func init() {
rootCmd.AddCommand(images.Cmd)
}

// Execute runs the root command.
func Execute() error {
return rootCmd.Execute()
}
35 changes: 35 additions & 0 deletions tools/labctl/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module github.com/GilmanLab/lab/tools/labctl

go 1.23.0

require (
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
Loading