chore: Improve workflow generation for container image projects (#674)

Co-authored-by: okozachenko1203 <okozachenko1203@users.noreply.github.com>
diff --git a/internal/pkg/image_repositories/build_workflow.go b/internal/pkg/image_repositories/build_workflow.go
index 5b81286..e66347b 100644
--- a/internal/pkg/image_repositories/build_workflow.go
+++ b/internal/pkg/image_repositories/build_workflow.go
@@ -1,6 +1,7 @@
 package image_repositories
 
 import (
+	"context"
 	"fmt"
 	"strings"
 )
@@ -101,8 +102,9 @@
 	return strings.Join(args.ToBuildArgs(), "\n")
 }
 
-func NewBuildWorkflow(project string) *GithubWorkflow {
+func NewBuildWorkflow(ctx context.Context, ir *ImageRepository) *GithubWorkflow {
 	extras := ""
+	project := ir.Project
 	if val, ok := EXTRAS[project]; ok {
 		extras = fmt.Sprintf("[%s]", val)
 	}
@@ -132,9 +134,19 @@
 		gitRepo = fmt.Sprintf("https://github.com/vexxhost/%s", project)
 	}
 
+	builderImageTag, err := getImageTag(ctx, ir.githubClient, "docker-openstack-builder", "openstack-builder-focal")
+	if err != nil {
+		builderImageTag = "latest"
+	}
+
+	runtimeImageTag, err := getImageTag(ctx, ir.githubClient, "docker-openstack-runtime", "openstack-runtime-focal")
+	if err != nil {
+		runtimeImageTag = "latest"
+	}
+
 	imageBuildArgs := ImageBuildArgs{
-		BuilderImage: "quay.io/vexxhost/openstack-builder-${{ matrix.from }}",
-		RuntimeImage: "quay.io/vexxhost/openstack-runtime-${{ matrix.from }}",
+		BuilderImage:  fmt.Sprintf("quay.io/vexxhost/openstack-builder-${{ matrix.from }}:%s", builderImageTag),
+		RuntimeImage: fmt.Sprintf("quay.io/vexxhost/openstack-runtime-${{ matrix.from }}:%s", runtimeImageTag),
 		Release:      "${{ matrix.release }}",
 		Project:      project,
 		ProjectRepo:  gitRepo,
@@ -144,6 +156,10 @@
 		DistPackages: distPackages,
 		PipPackages:  pipPackages,
 	}
+	imageVerifyCmds := []string{
+		fmt.Sprintf("cosign verify --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity=https://github.com/vexxhost/docker-openstack-builder/.github/workflows/build.yml@refs/heads/main quay.io/vexxhost/openstack-builder-${{ matrix.from }}:%s", builderImageTag),
+		fmt.Sprintf("cosign verify --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity=https://github.com/vexxhost/docker-openstack-runtime/.github/workflows/build.yml@refs/heads/main quay.io/vexxhost/openstack-runtime-${{ matrix.from }}:%s", runtimeImageTag),
+	}
 
 	releases := []string{"wallaby", "xena", "yoga", "zed", "2023.1"}
 	if project == "keystone" {
@@ -170,6 +186,12 @@
 		Jobs: map[string]GithubWorkflowJob{
 			"image": {
 				RunsOn: "ubuntu-latest",
+				Permissions: map[string]string{
+					"actions": "read",
+					"contents": "read",
+					"id-token": "write",
+					"security-events": "write",
+				},
 				Strategy: GithubWorkflowStrategy{
 					Matrix: map[string]interface{}{
 						"from":    []string{"focal", "jammy"},
@@ -222,16 +244,62 @@
 						},
 					},
 					{
+						Name: "Install cosign",
+						Uses: "sigstore/cosign-installer@main",
+					},
+					{
+						Name: "Verify images",
+						Run: strings.Join(imageVerifyCmds, "\n"),
+					},
+					{
 						Name: "Build image",
 						Uses: "docker/build-push-action@v3",
+						Environment: map[string]string{
+							"DOCKER_CONTENT_TRUST": "1",
+						},
+						With: map[string]string{
+							"context":    ".",
+							"cache-from": "type=gha,scope=${{ matrix.from }}-${{ matrix.release }}",
+							"cache-to":   "type=gha,mode=max,scope=${{ matrix.from }}-${{ matrix.release }}",
+							"load": "true",
+							"build-args": imageBuildArgs.ToBuildArgsString(),
+							"tags":       fmt.Sprintf("quay.io/vexxhost/%s:${{ env.PROJECT_REF }}-${{ matrix.from }}-${{ github.sha }}", project),
+						},
+					},
+					{
+						Name: "Scan image for vulnerabilities",
+						Uses: "aquasecurity/trivy-action@master",
+						With: map[string]string{
+							"image-ref": fmt.Sprintf("quay.io/vexxhost/%s:${{ env.PROJECT_REF }}-${{ matrix.from }}-${{ github.sha }}", project),
+							"format": "sarif",
+							"output": "trivy-results.sarif",
+							"ignore-unfixed": "true",
+						},
+					},
+					{
+						Name: "Upload scan result",
+						Uses: "github/codeql-action/upload-sarif@v2",
+						If:   "always()",
+						With: map[string]string{
+							"category": "${{ env.PROJECT_REF }}-${{ matrix.from }}",
+							"sarif_file": "trivy-results.sarif",
+						},
+					},
+					{
+						Name: "Build image",
+						Uses: "docker/build-push-action@v3",
+						Environment: map[string]string{
+							"DOCKER_CONTENT_TRUST": "1",
+						},
 						With: map[string]string{
 							"context":    ".",
 							"cache-from": "type=gha,scope=${{ matrix.from }}-${{ matrix.release }}",
 							"cache-to":   "type=gha,mode=max,scope=${{ matrix.from }}-${{ matrix.release }}",
 							"platforms":  platforms,
+							"sbom": "true",
 							"push":       "${{ github.event_name == 'push' }}",
 							"build-args": imageBuildArgs.ToBuildArgsString(),
-							"tags":       fmt.Sprintf("quay.io/vexxhost/%s:${{ env.PROJECT_REF }}-${{ matrix.from }}", project),
+							"tags":       fmt.Sprintf("quay.io/vexxhost/%s:${{ env.PROJECT_REF }}-${{ matrix.from }}-${{ github.sha }}", project),
 						},
 					},
 					{
@@ -239,10 +307,15 @@
 						Uses: "akhilerm/tag-push-action@v2.0.0",
 						If:   `github.event_name == 'push' && ((matrix.from == 'focal') || (matrix.from == 'jammy' && matrix.release != 'yoga'))`,
 						With: map[string]string{
-							"src": fmt.Sprintf("quay.io/vexxhost/%s:${{ env.PROJECT_REF }}-${{ matrix.from }}", project),
+							"src": fmt.Sprintf("quay.io/vexxhost/%s:${{ env.PROJECT_REF }}-${{ matrix.from }}-${{ github.sha }}", project),
 							"dst": fmt.Sprintf("quay.io/vexxhost/%s:${{ matrix.release }}", project),
 						},
 					},
+					{
+						Name: "Sign the container image",
+						If:   "${{ github.event_name == 'push' }}",
+						Run: "cosign sign --yes quay.io/vexxhost/horizon@${{ steps.push-step.outputs.digest }}",
+					},
 				},
 			},
 		},
diff --git a/internal/pkg/image_repositories/dockerfile.go b/internal/pkg/image_repositories/dockerfile.go
index 4fed1af..f4c09cc 100644
--- a/internal/pkg/image_repositories/dockerfile.go
+++ b/internal/pkg/image_repositories/dockerfile.go
@@ -21,10 +21,6 @@
 
 	BindepImage     string
 	BindepImageTag  string
-	BuilderImage    string
-	BuilderImageTag string
-	RuntimeImage    string
-	RuntimeImageTag string
 
 	template *template.Template
 }
@@ -35,25 +31,11 @@
 		return nil, err
 	}
 
-	builderImageTag, err := getImageTag(ctx, ir.githubClient, "docker-openstack-builder", "openstack-builder-focal")
-	if err != nil {
-		return nil, err
-	}
-
-	runtimeImageTag, err := getImageTag(ctx, ir.githubClient, "docker-openstack-runtime", "openstack-runtime-focal")
-	if err != nil {
-		return nil, err
-	}
-
 	return &Dockerfile{
 		Project: ir.Project,
 
 		BindepImage:     "quay.io/vexxhost/bindep-loci",
 		BindepImageTag:  "latest",
-		BuilderImage:    "quay.io/vexxhost/openstack-builder-focal",
-		BuilderImageTag: builderImageTag,
-		RuntimeImage:    "quay.io/vexxhost/openstack-runtime-focal",
-		RuntimeImageTag: runtimeImageTag,
 
 		template: tmpl,
 	}, nil
diff --git a/internal/pkg/image_repositories/github_workflow.go b/internal/pkg/image_repositories/github_workflow.go
index 2b1fefd..59aa047 100644
--- a/internal/pkg/image_repositories/github_workflow.go
+++ b/internal/pkg/image_repositories/github_workflow.go
@@ -50,9 +50,10 @@
 }
 
 type GithubWorkflowJob struct {
-	RunsOn   string                 `yaml:"runs-on"`
-	Strategy GithubWorkflowStrategy `yaml:"strategy,omitempty"`
-	Steps    []GithubWorkflowStep   `yaml:"steps"`
+	Permissions map[string]string `yaml:"permissions,omitempty"`
+	RunsOn      string                 `yaml:"runs-on"`
+	Strategy    GithubWorkflowStrategy `yaml:"strategy,omitempty"`
+	Steps       []GithubWorkflowStep   `yaml:"steps"`
 }
 
 func (j GithubWorkflowJob) DeepCopy() GithubWorkflowJob {
diff --git a/internal/pkg/image_repositories/image_repository.go b/internal/pkg/image_repositories/image_repository.go
index 0d2f369..d45a759 100644
--- a/internal/pkg/image_repositories/image_repository.go
+++ b/internal/pkg/image_repositories/image_repository.go
@@ -51,7 +51,7 @@
 
 func (i *ImageRepository) WriteFiles(ctx context.Context, fs billy.Filesystem) error {
 	// .github/workflows/build.yml
-	build := NewBuildWorkflow(i.Project)
+	build := NewBuildWorkflow(ctx, i)
 	err := build.WriteFile(fs)
 	if err != nil {
 		return err
diff --git a/internal/pkg/image_repositories/template/Dockerfile b/internal/pkg/image_repositories/template/Dockerfile
index 98edce3..830a633 100644
--- a/internal/pkg/image_repositories/template/Dockerfile
+++ b/internal/pkg/image_repositories/template/Dockerfile
@@ -1,14 +1,14 @@
 # syntax=docker/dockerfile-upstream:master-labs
 
-ARG BUILDER_IMAGE={{ .BuilderImage }}
-ARG RUNTIME_IMAGE={{ .RuntimeImage }}
+ARG BUILDER_IMAGE
+ARG RUNTIME_IMAGE
 
 FROM {{ .BindepImage }}:{{ .BindepImageTag }} AS bindep
 
-FROM ${BUILDER_IMAGE}:{{ .BuilderImageTag }} AS builder
+FROM ${BUILDER_IMAGE} AS builder
 COPY --from=bindep --link /runtime-pip-packages /runtime-pip-packages
 
-FROM ${RUNTIME_IMAGE}:{{ .RuntimeImageTag }} AS runtime
+FROM ${RUNTIME_IMAGE} AS runtime
 COPY --from=bindep --link /runtime-dist-packages /runtime-dist-packages
 COPY --from=builder --link /var/lib/openstack /var/lib/openstack