diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..170bc4b --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,20 @@ +name: golangci-lint +on: + push: + branches: + - main + pull_request: + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1f07e6c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Test +on: + push: + branches: + - main + pull_request: + +jobs: + test: + name: tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: Test + run: go test -v ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 600d2d3..33fb288 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode \ No newline at end of file +.vscode +imager \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..4d2d576 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,53 @@ +# Options for analysis running. +run: + timeout: 3m + concurrency: 8 + +issues: + exclude-dirs: + - pkg/mycarehub/presentation/graph/ + - ./tests/ + +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - asciicheck + - dogsled + - goheader + - unused + - misspell + - rowserrcheck + - sqlclosecheck + - revive + - funlen + - gofmt + - unparam + - errorlint + - bodyclose + - gocritic + - nilerr + - ireturn + - importas + - wsl + - copyloopvar + - nilerr + - makezero + - reassign + +linters-settings: + staticcheck: + checks: ["all"] + funlen: + lines: -1 + statements: -1 + gosec: + excludes: + - G601 + - G304 + - G101 diff --git a/README.md b/README.md index 952a179..775b4d4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,46 @@ -It is a simple utility that uses a link to a helm chart to search for container images defined in the chart, downloads the respective Docker images from their repositories, and prints info about the images i.e their sizes and number of layers. +# Imager -```bash -go run cmd/main.go list https://github.com/openfga/helm-charts/releases/download/openfga-0.2.19/openfga-0.2.19.tgz -``` +A simple utility that uses a link to a Helm chart to search for container images defined in the chart, downloads the respective images and outputs info about the images i.e., their sizes and number of layers. + +## Features + +- Extracts container images from Helm charts. +- Downloads the specified images. +- Displays image size. +- Displays the number of image layers. + +## Installation + +### Prerequisites + +- Go 1.23 or higher + +### Building and Running + +1. Build the `imager` binary + + ```bash + go build -o imager ./cmd/main.go + ``` + This command will create an executable file named imager in your current directory. + +2. Run imager with the desired option + ```bash + ./imager + ``` + Available subcommands: + - `list`: A CLI to show image information. + Example + ```bash + ./imager list https://github.com/openfga/helm-charts/releases/download/openfga-0.2.19/openfga-0.2.19.tgz + ``` + - `http-server`: Starts a HTTP server that exposes an API to show image information. + ```bash + ./imager http-server + ``` + Example request: + ```bash + curl --location 'http://localhost:8080' --header 'Content-Type: application/json' --data '{"chart_url": "oci://registry-1.docker.io/bitnamicharts/airflow"}' + ``` + + \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 02db719..bb893cc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -26,9 +26,9 @@ func main() { return cli.Exit("Expected a link to a helm chart", 1) } - chartUrl := cCtx.Args().First() + chartURL := cCtx.Args().First() - images, err := i.GetChartImagesDetails(cCtx.Context, chartUrl) + images, err := i.GetChartImagesDetails(cCtx.Context, chartURL) if err != nil { return cli.Exit(err.Error(), 1) } diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index 511b270..ec62618 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -60,7 +60,7 @@ func NewImager() *Imager { func (i Imager) extractImages(ctx context.Context, chart *chart.Chart) ([]string, error) { release, err := i.client.RunWithContext(ctx, chart, map[string]interface{}{}) if err != nil { - return nil, fmt.Errorf("Error rendering templates: %v", err) + return nil, fmt.Errorf("Error rendering templates: %w", err) } reader := strings.NewReader(release.Manifest) @@ -76,7 +76,7 @@ func (i Imager) extractImages(ctx context.Context, chart *chart.Chart) ([]string break } - return nil, fmt.Errorf("Failed to decode manifest: %v", err) + return nil, fmt.Errorf("Failed to decode manifest: %w", err) } obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(rawObj.Raw, nil, nil) @@ -105,23 +105,22 @@ func (i Imager) extractImages(ctx context.Context, chart *chart.Chart) ([]string func (i Imager) getImageDetails(_ context.Context, containerImage string) (*ImageDetails, error) { ref, err := name.ParseReference(containerImage) if err != nil { - return nil, fmt.Errorf("failed to parse image reference: %v", err) - + return nil, fmt.Errorf("failed to parse image reference: %w", err) } image, err := remote.Image(ref) if err != nil { - return nil, fmt.Errorf("failed to pull image: %v", err) + return nil, fmt.Errorf("failed to pull image: %w", err) } layers, err := image.Layers() if err != nil { - return nil, fmt.Errorf("failed to get image layers: %v", err) + return nil, fmt.Errorf("failed to get image layers: %w", err) } size, err := image.Size() if err != nil { - return nil, fmt.Errorf("failed to get image size: %v", err) + return nil, fmt.Errorf("failed to get image size: %w", err) } details := ImageDetails{ @@ -137,18 +136,17 @@ func (i Imager) getImageDetails(_ context.Context, containerImage string) (*Imag func (i Imager) GetChartImagesDetails(ctx context.Context, chartURL string) ([]*ImageDetails, error) { chartPath, err := i.client.ChartPathOptions.LocateChart(chartURL, settings) if err != nil { - return nil, fmt.Errorf("failed to locate chart from provided path: %v", err) + return nil, fmt.Errorf("failed to locate chart from provided path: %w", err) } chart, err := loader.Load(chartPath) if err != nil { - return nil, fmt.Errorf("failed to load chart: %v", err) - + return nil, fmt.Errorf("failed to load chart: %w", err) } containerImages, err := i.extractImages(ctx, chart) if err != nil { - return nil, fmt.Errorf("failed to get images from chart: %v", err) + return nil, fmt.Errorf("failed to get images from chart: %w", err) } if len(containerImages) == 0 { @@ -164,7 +162,7 @@ func (i Imager) GetChartImagesDetails(ctx context.Context, chartURL string) ([]* for _, containerImage := range containerImages { imageInfo, err := i.getImageDetails(ctx, containerImage) if err != nil { - return nil, fmt.Errorf("failed to get images from main chart: %v", err) + return nil, fmt.Errorf("failed to get images from main chart: %w", err) } images = append(images, imageInfo) diff --git a/pkg/imager/imager_test.go b/pkg/imager/imager_test.go index d3007db..3845e5f 100644 --- a/pkg/imager/imager_test.go +++ b/pkg/imager/imager_test.go @@ -9,11 +9,11 @@ import ( ) func TestImager_GetChartImagesDetails(t *testing.T) { - type args struct { ctx context.Context chartURL string } + tests := []struct { name string args args @@ -63,11 +63,13 @@ func TestImager_GetChartImagesDetails(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { i := imager.NewImager() + got, err := i.GetChartImagesDetails(tt.args.ctx, tt.args.chartURL) if (err != nil) != tt.wantErr { t.Errorf("Imager.GetChartImagesDetails() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("Imager.GetChartImagesDetails() = %v, want %v", got, tt.want) }