diff --git a/cmd/client.go b/cmd/client.go index 3c3a1761a6..b83b3fd4fa 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -62,7 +62,7 @@ func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { t = newTransport(cfg.InsecureSkipVerify) // may provide a custom impl which proxies c = newCredentialsProvider(config.Dir(), t, "") // for accessing registries d = newKnativeDeployer(cfg.Verbose) // default deployer (can be overridden via options) - pp = newTektonPipelinesProvider(c, cfg.Verbose) + pp = newTektonPipelinesProvider(c, cfg.Verbose, t) o = []fn.Option{ // standard (shared) options for all commands fn.WithVerbose(cfg.Verbose), fn.WithTransport(t), @@ -70,7 +70,11 @@ func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { fn.WithScaffolder(buildpacks.NewScaffolder(cfg.Verbose)), fn.WithBuilder(buildpacks.NewBuilder(buildpacks.WithVerbose(cfg.Verbose))), fn.WithRemovers(knative.NewRemover(cfg.Verbose), k8s.NewRemover(cfg.Verbose), keda.NewRemover(cfg.Verbose)), - fn.WithDescribers(knative.NewDescriber(cfg.Verbose), k8s.NewDescriber(cfg.Verbose), keda.NewDescriber(cfg.Verbose)), + fn.WithDescribers( + knative.NewDescriber(cfg.Verbose, knative.WithDescriberTransport(t)), + k8s.NewDescriber(cfg.Verbose, k8s.WithDescriberTransport(t)), + keda.NewDescriber(cfg.Verbose, keda.WithDescriberTransport(t)), + ), fn.WithListers(knative.NewLister(cfg.Verbose), k8s.NewLister(cfg.Verbose), keda.NewLister(cfg.Verbose)), fn.WithDeployer(d), fn.WithPipelinesProvider(pp), @@ -143,11 +147,12 @@ func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath return creds.NewCredentialsProvider(configPath, options...) } -func newTektonPipelinesProvider(creds oci.CredentialsProvider, verbose bool) *tekton.PipelinesProvider { +func newTektonPipelinesProvider(creds oci.CredentialsProvider, verbose bool, transport http.RoundTripper) *tekton.PipelinesProvider { options := []tekton.Opt{ tekton.WithCredentialsProvider(creds), tekton.WithVerbose(verbose), tekton.WithPipelineDecorator(deployDecorator{}), + tekton.WithTransport(transport), } return tekton.NewPipelinesProvider(options...) diff --git a/cmd/deploy.go b/cmd/deploy.go index b4c8b32411..c12fdf88ed 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -782,7 +782,7 @@ func (c deployConfig) clientOptions() ([]fn.Option, error) { // Override the pipelines provider to use custom credentials // This is needed for remote builds (deploy --remote) - o = append(o, fn.WithPipelinesProvider(newTektonPipelinesProvider(creds, c.Verbose))) + o = append(o, fn.WithPipelinesProvider(newTektonPipelinesProvider(creds, c.Verbose, t))) // Add the appropriate deployer based on deploy type deployer := c.Deployer diff --git a/cmd/func-util/s2i_generate.go b/cmd/func-util/s2i_generate.go index 36f51cc598..0293716841 100644 --- a/cmd/func-util/s2i_generate.go +++ b/cmd/func-util/s2i_generate.go @@ -36,6 +36,7 @@ type genConfig struct { imageScriptUrl string logLevel string middlewareVersion string + commit string envVars []string } @@ -64,6 +65,7 @@ func newS2IGenerateCmd() *cobra.Command { genCmd.Flags().StringVar(&config.imageScriptUrl, "image-script-url", "image:///usr/libexec/s2i", "") genCmd.Flags().StringVar(&config.logLevel, "log-level", "0", "") genCmd.Flags().StringVar(&config.middlewareVersion, "middleware-version", "", "") + genCmd.Flags().StringVar(&config.commit, "commit", "", "") return genCmd } @@ -142,6 +144,10 @@ func runS2IGenerate(ctx context.Context, c genConfig) error { }, } + if c.commit != "" { + s2iConfig.Labels[fn.CommitLabelKey] = c.commit + } + builder, _, err := strategies.Strategy(nil, &s2iConfig, build.Overrides{}) if err != nil { return fmt.Errorf("cannot create builder: %w", err) diff --git a/pkg/buildpacks/builder.go b/pkg/buildpacks/builder.go index 1a3fb44f26..b806a02cd1 100644 --- a/pkg/buildpacks/builder.go +++ b/pkg/buildpacks/builder.go @@ -201,13 +201,27 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf opts.Env["BP_GO_WORKDIR"] = filepath.Join(fn.RunDataDir, fn.BuildDir) } - // Get middleware version and set as image label via BP_IMAGE_LABELS + // Set image labels via BP_IMAGE_LABELS + var imageLabels []string + middlewareVersion, err := scaffolding.MiddlewareVersion(f.Root, f.Runtime, f.Invoke, fn.EmbeddedTemplatesFS) if err != nil { return fmt.Errorf("cannot get middleware version: %w", err) } if middlewareVersion != "" { - opts.Env["BP_IMAGE_LABELS"] = fmt.Sprintf("%s=%s", fn.MiddlewareVersionLabelKey, middlewareVersion) + imageLabels = append(imageLabels, fmt.Sprintf("%s=%s", fn.MiddlewareVersionLabelKey, middlewareVersion)) + } + + commit, err := fn.GitCommit(f.Root) + if err != nil { + return fmt.Errorf("cannot get git commit: %w", err) + } + if commit != "" { + imageLabels = append(imageLabels, fmt.Sprintf("%s=%s", fn.CommitLabelKey, commit)) + } + + if len(imageLabels) > 0 { + opts.Env["BP_IMAGE_LABELS"] = strings.Join(imageLabels, " ") } var bindings = make([]string, 0, len(f.Build.Mounts)) diff --git a/pkg/functions/client.go b/pkg/functions/client.go index bf06f3f2ce..54b9c1d115 100644 --- a/pkg/functions/client.go +++ b/pkg/functions/client.go @@ -188,6 +188,7 @@ type Instance struct { Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"` Labels map[string]string `json:"labels" yaml:"labels" xml:"-"` Middleware Middleware `json:"middleware,omitempty" yaml:"middleware,omitempty"` + Revision string `json:"revision,omitempty" yaml:"revision,omitempty"` Generation int64 `json:"generation,omitempty" yaml:"generation,omitempty"` Ready string `json:"ready,omitempty" yaml:"ready,omitempty"` } diff --git a/pkg/functions/function_labels.go b/pkg/functions/function_labels.go index 1602c06cd4..9f6a67035f 100644 --- a/pkg/functions/function_labels.go +++ b/pkg/functions/function_labels.go @@ -2,14 +2,18 @@ package functions import ( "fmt" + "net/http" "os" "strings" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" "knative.dev/func/pkg/utils" ) const ( MiddlewareVersionLabelKey = "middleware-version" + CommitLabelKey = "org.opencontainers.image.revision" ) type Label struct { @@ -77,3 +81,27 @@ func ValidateLabels(labels []Label) (errors []string) { return } + +func ImageLabels(image string, transport http.RoundTripper) (map[string]string, error) { + ref, err := name.ParseReference(image) + if err != nil { + return nil, err + } + + desc, err := remote.Get(ref, remote.WithTransport(transport)) + if err != nil { + return nil, err + } + + img, err := desc.Image() + if err != nil { + return nil, err + } + + cfg, err := img.ConfigFile() + if err != nil { + return nil, err + } + + return cfg.Config.Labels, nil +} diff --git a/pkg/functions/git_commit.go b/pkg/functions/git_commit.go new file mode 100644 index 0000000000..d4ecdb351d --- /dev/null +++ b/pkg/functions/git_commit.go @@ -0,0 +1,41 @@ +package functions + +import ( + "github.com/go-git/go-git/v5" +) + +// GitCommit returns the short commit SHA of the git repository containing +// the given directory. Returns "-dirty" if the working tree has +// uncommitted changes. Returns an empty string if the directory is not +// inside a git repository. +func GitCommit(dir string) (string, error) { + repo, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{ + DetectDotGit: true, + }) + if err != nil { + return "", nil + } + + head, err := repo.Head() + if err != nil { + return "", nil + } + + sha := head.Hash().String()[:7] + + wt, err := repo.Worktree() + if err != nil { + return sha, nil + } + + status, err := wt.Status() + if err != nil { + return sha, nil + } + + if !status.IsClean() { + sha += "-dirty" + } + + return sha, nil +} diff --git a/pkg/functions/middleware.go b/pkg/functions/middleware.go index 4d13799175..46211a916f 100644 --- a/pkg/functions/middleware.go +++ b/pkg/functions/middleware.go @@ -1,42 +1,9 @@ package functions import ( - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" "knative.dev/func/pkg/scaffolding" ) -// MiddlewareVersion gets the used middleware version of a function image. -// Returns an empty string and no error in case the function image was built -// without this information. -func MiddlewareVersion(image string) (string, error) { - ref, err := name.ParseReference(image) - if err != nil { - return "", err - } - - desc, err := remote.Get(ref) - if err != nil { - return "", err - } - - img, err := desc.Image() - if err != nil { - return "", err - } - - cfg, err := img.ConfigFile() - if err != nil { - return "", err - } - - if cfg.Config.Labels == nil { - return "", nil - } - - return cfg.Config.Labels[MiddlewareVersionLabelKey], nil -} - func LatestMiddlewareVersions() (map[string]map[string]string, error) { return scaffolding.MiddlewareVersions(EmbeddedTemplatesFS) } diff --git a/pkg/k8s/describer.go b/pkg/k8s/describer.go index 0646023f0f..14f8468fdf 100644 --- a/pkg/k8s/describer.go +++ b/pkg/k8s/describer.go @@ -3,6 +3,7 @@ package k8s import ( "context" "fmt" + "net/http" "strings" v1 "k8s.io/api/apps/v1" @@ -13,13 +14,24 @@ import ( ) type Describer struct { - verbose bool + verbose bool + transport http.RoundTripper } -func NewDescriber(verbose bool) *Describer { - return &Describer{ - verbose: verbose, +type DescriberOpt func(*Describer) + +func WithDescriberTransport(transport http.RoundTripper) DescriberOpt { + return func(d *Describer) { + d.transport = transport + } +} + +func NewDescriber(verbose bool, opts ...DescriberOpt) *Describer { + d := &Describer{verbose: verbose} + for _, o := range opts { + o(d) } + return d } // Describe a function by name. @@ -77,11 +89,12 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In } middlewareVersion := "" - if image != "" { - v, err := fn.MiddlewareVersion(image) + commit := "" + if image != "" && d.transport != nil { + labels, err := fn.ImageLabels(image, d.transport) if err == nil { - // don't fail on errors - middlewareVersion = v + middlewareVersion = labels[fn.MiddlewareVersionLabelKey] + commit = labels[fn.CommitLabelKey] } } @@ -96,6 +109,7 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In Middleware: fn.Middleware{ Version: middlewareVersion, }, + Revision: commit, Generation: deployment.Generation, Ready: strings.ToLower(string(ready)), } diff --git a/pkg/keda/describer.go b/pkg/keda/describer.go index 4c8fd6b212..946f6e2a44 100644 --- a/pkg/keda/describer.go +++ b/pkg/keda/describer.go @@ -3,6 +3,7 @@ package keda import ( "context" "fmt" + "net/http" "strings" "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" @@ -15,13 +16,24 @@ import ( ) type Describer struct { - verbose bool + verbose bool + transport http.RoundTripper } -func NewDescriber(verbose bool) *Describer { - return &Describer{ - verbose: verbose, +type DescriberOpt func(*Describer) + +func WithDescriberTransport(transport http.RoundTripper) DescriberOpt { + return func(d *Describer) { + d.transport = transport + } +} + +func NewDescriber(verbose bool, opts ...DescriberOpt) *Describer { + d := &Describer{verbose: verbose} + for _, o := range opts { + o(d) } + return d } // Describe a function by name. @@ -96,11 +108,12 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In } middlewareVersion := "" - if image != "" { - v, err := fn.MiddlewareVersion(image) + commit := "" + if image != "" && d.transport != nil { + labels, err := fn.ImageLabels(image, d.transport) if err == nil { - // don't fail on errors - middlewareVersion = v + middlewareVersion = labels[fn.MiddlewareVersionLabelKey] + commit = labels[fn.CommitLabelKey] } } @@ -115,6 +128,7 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In Middleware: fn.Middleware{ Version: middlewareVersion, }, + Revision: commit, Generation: deployment.Generation, Ready: strings.ToLower(string(ready)), } diff --git a/pkg/knative/describer.go b/pkg/knative/describer.go index e0a6ddca33..37de836cd8 100644 --- a/pkg/knative/describer.go +++ b/pkg/knative/describer.go @@ -3,6 +3,7 @@ package knative import ( "context" "fmt" + "net/http" "strings" corev1 "k8s.io/api/core/v1" @@ -16,13 +17,24 @@ import ( ) type Describer struct { - verbose bool + verbose bool + transport http.RoundTripper } -func NewDescriber(verbose bool) *Describer { - return &Describer{ - verbose: verbose, +type DescriberOpt func(*Describer) + +func WithDescriberTransport(transport http.RoundTripper) DescriberOpt { + return func(d *Describer) { + d.transport = transport + } +} + +func NewDescriber(verbose bool, opts ...DescriberOpt) *Describer { + d := &Describer{verbose: verbose} + for _, o := range opts { + o(d) } + return d } // Describe a function by name. Note that the consuming API uses domain style @@ -127,13 +139,13 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In } } - if description.Image != "" { - v, err := fn.MiddlewareVersion(description.Image) + if description.Image != "" && d.transport != nil { + labels, err := fn.ImageLabels(description.Image, d.transport) if err == nil { - // don't fail on errors description.Middleware = fn.Middleware{ - Version: v, + Version: labels[fn.MiddlewareVersionLabelKey], } + description.Revision = labels[fn.CommitLabelKey] } } diff --git a/pkg/oci/builder.go b/pkg/oci/builder.go index 20dd15b7b1..be06fd3423 100644 --- a/pkg/oci/builder.go +++ b/pkg/oci/builder.go @@ -114,6 +114,10 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, pp []fn.Platform) (e return } + if err = fetchCommitLabel(&job); err != nil { + return + } + if err = containerize(job); err != nil { // write image to .func/build return } @@ -200,6 +204,21 @@ func fetchMiddlewareLabels(job *buildJob) (err error) { return nil } +// fetchCommitLabel retrieves git commit SHA for image labels +func fetchCommitLabel(job *buildJob) error { + commit, err := fn.GitCommit(job.function.Root) + if err != nil { + return fmt.Errorf("unable to get git commit: %w", err) + } + if commit != "" { + if job.labels == nil { + job.labels = make(map[string]string) + } + job.labels[fn.CommitLabelKey] = commit + } + return nil +} + // containerize the full service which consists of the scaffolded Function, // Function implementation, base image, data layers etc. // This container is stored on disk for later upload to the registry via diff --git a/pkg/pipelines/tekton/pipelines_provider.go b/pkg/pipelines/tekton/pipelines_provider.go index d50801cc22..15182ecc12 100644 --- a/pkg/pipelines/tekton/pipelines_provider.go +++ b/pkg/pipelines/tekton/pipelines_provider.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/fs" + "net/http" "os" "path" "path/filepath" @@ -56,6 +57,7 @@ type PipelinesProvider struct { getPacURL pacURLCallback credentialsProvider oci.CredentialsProvider decorator PipelineDecorator + transport http.RoundTripper } func WithCredentialsProvider(credentialsProvider oci.CredentialsProvider) Opt { @@ -76,6 +78,12 @@ func WithPipelineDecorator(decorator PipelineDecorator) Opt { } } +func WithTransport(transport http.RoundTripper) Opt { + return func(pp *PipelinesProvider) { + pp.transport = transport + } +} + func WithPacURLCallback(getPacURL pacURLCallback) Opt { return func(pp *PipelinesProvider) { pp.getPacURL = getPacURL @@ -245,12 +253,12 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) (string, fn var describer fn.Describer switch f.Deploy.Deployer { case k8s.KubernetesDeployerName: - describer = k8s.NewDescriber(false) + describer = k8s.NewDescriber(false, k8s.WithDescriberTransport(pp.transport)) case keda.KedaDeployerName: - describer = keda.NewDescriber(false) + describer = keda.NewDescriber(false, keda.WithDescriberTransport(pp.transport)) default: // default to knative - describer = knative.NewDescriber(false) + describer = knative.NewDescriber(false, knative.WithDescriberTransport(pp.transport)) } obj, err := describer.Describe(ctx, f.Name, f.Namespace) diff --git a/pkg/pipelines/tekton/task-buildpack.yaml.tmpl b/pkg/pipelines/tekton/task-buildpack.yaml.tmpl index a9bea7d83d..2fafd465f4 100644 --- a/pkg/pipelines/tekton/task-buildpack.yaml.tmpl +++ b/pkg/pipelines/tekton/task-buildpack.yaml.tmpl @@ -64,6 +64,9 @@ spec: - name: INSECURE_REGISTRIES description: Registries to access without TLS verification default: "" + - name: COMMIT + description: Git commit SHA of the function source + default: "" stepTemplate: env: - name: CNB_PLATFORM_API @@ -156,6 +159,13 @@ spec: echo "No middleware-version file ($middleware_version_file) found" fi + # Add revision label + commit="$(params.COMMIT)" + if [ -n "$commit" ]; then + echo "--> Adding revision label: $commit" + echo -n " org.opencontainers.image.revision=$commit" >> "${ENV_DIR}/BP_IMAGE_LABELS" + fi + ############################################ ##### Added part for Knative Functions ##### ############################################ diff --git a/pkg/pipelines/tekton/task-s2i.yaml.tmpl b/pkg/pipelines/tekton/task-s2i.yaml.tmpl index b79c86d239..c739a45a1f 100644 --- a/pkg/pipelines/tekton/task-s2i.yaml.tmpl +++ b/pkg/pipelines/tekton/task-s2i.yaml.tmpl @@ -42,6 +42,9 @@ spec: - name: S2I_IMAGE_SCRIPTS_URL description: The URL containing the default assemble and run scripts for the builder image. default: "image:///usr/libexec/s2i" + - name: COMMIT + description: Git commit SHA of the function source + default: "" workspaces: - name: source - name: cache @@ -96,6 +99,8 @@ spec: - $(params.S2I_IMAGE_SCRIPTS_URL) - "--log-level" - $(params.LOGLEVEL) + - "--commit" + - $(params.COMMIT) - $(params.ENV_VARS[*]) volumeMounts: - mountPath: /gen-source diff --git a/pkg/pipelines/tekton/templates.go b/pkg/pipelines/tekton/templates.go index 79873f96c3..b853194bcf 100644 --- a/pkg/pipelines/tekton/templates.go +++ b/pkg/pipelines/tekton/templates.go @@ -90,6 +90,9 @@ type templateData struct { // TLS verification for registry operations TlsVerify string + + // Git commit SHA of the function source + Commit string } // createPipelineTemplatePAC creates a Pipeline template used for PAC on-cluster build @@ -386,6 +389,8 @@ func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels m tlsVerify = "false" } + commit, _ := fn.GitCommit(f.Root) + data := templateData{ FunctionName: f.Name, Annotations: f.Deploy.Annotations, @@ -403,6 +408,7 @@ func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels m S2iImageScriptsUrl: s2iImageScriptsUrl, TlsVerify: tlsVerify, + Commit: commit, RepoUrl: f.Build.Git.URL, Revision: pipelinesTargetBranch, diff --git a/pkg/pipelines/tekton/templates_pack.go b/pkg/pipelines/tekton/templates_pack.go index 7b5dcd02d9..d4e73dc4b7 100644 --- a/pkg/pipelines/tekton/templates_pack.go +++ b/pkg/pipelines/tekton/templates_pack.go @@ -40,6 +40,10 @@ spec: - description: Environment variables to set during build time name: buildEnvs type: array + - description: Git commit SHA of the function source + name: commit + default: '' + type: string tasks: - name: build params: @@ -58,6 +62,8 @@ spec: - name: ENV_VARS value: - '$(params.buildEnvs[*])' + - name: COMMIT + value: $(params.commit) {{- if eq .TlsVerify "false"}} - name: INSECURE_REGISTRIES value: $(params.registry) @@ -115,6 +121,8 @@ spec: {{range .BuildEnvs -}} - {{.}} {{end}} + - name: commit + value: "{{.Commit}}" pipelineRef: name: {{.PipelineName}} workspaces: diff --git a/pkg/pipelines/tekton/templates_s2i.go b/pkg/pipelines/tekton/templates_s2i.go index 9441a9c5c6..8eb5ccd7a0 100644 --- a/pkg/pipelines/tekton/templates_s2i.go +++ b/pkg/pipelines/tekton/templates_s2i.go @@ -48,6 +48,10 @@ spec: name: tlsVerify type: string default: 'true' + - description: Git commit SHA of the function source + name: commit + default: '' + type: string tasks: - name: build params: @@ -70,6 +74,8 @@ spec: value: $(params.s2iImageScriptsUrl) - name: TLSVERIFY value: $(params.tlsVerify) + - name: COMMIT + value: $(params.commit) {{.FuncS2iTaskRef}} workspaces: - name: source @@ -126,6 +132,8 @@ spec: value: {{.S2iImageScriptsUrl}} - name: tlsVerify value: {{.TlsVerify}} + - name: commit + value: "{{.Commit}}" pipelineRef: name: {{.PipelineName}} workspaces: diff --git a/pkg/s2i/builder.go b/pkg/s2i/builder.go index b2d939ede6..842518e87b 100644 --- a/pkg/s2i/builder.go +++ b/pkg/s2i/builder.go @@ -184,13 +184,23 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf cfg.ScriptsURL = "file://" + f.Root + "/" + fn.RunDataDir + "/" + fn.BuildDir + "/bin" } - // Set middleware version label + // Set image labels + cfg.Labels = make(map[string]string) + middlewareVersion, err := scaffolding.MiddlewareVersion(f.Root, f.Runtime, f.Invoke, fn.EmbeddedTemplatesFS) if err != nil { return fmt.Errorf("cannot get middleware version: %w", err) } if middlewareVersion != "" { - cfg.Labels = map[string]string{fn.MiddlewareVersionLabelKey: middlewareVersion} + cfg.Labels[fn.MiddlewareVersionLabelKey] = middlewareVersion + } + + commit, err := fn.GitCommit(f.Root) + if err != nil { + return fmt.Errorf("cannot get git commit: %w", err) + } + if commit != "" { + cfg.Labels[fn.CommitLabelKey] = commit } // Environment variables