Skip to content

Commit

Permalink
feat: Add template params for git
Browse files Browse the repository at this point in the history
This includes a number of template parameters supported by [goreleaser](https://goreleaser.com/customization/templates/). Specifically, the build date information and the majority of the Git params.

Majority of the code is copied from goreleaser. I've added the MIT license from goreleaser at the top of the files.

Fixes ko-build#493

Signed-off-by: Nathan Mittler <nmittler@aviatrix.com>
  • Loading branch information
nmittler committed May 15, 2024
1 parent d432560 commit bd46af2
Show file tree
Hide file tree
Showing 9 changed files with 1,236 additions and 23 deletions.
30 changes: 26 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,32 @@ of the `ko` process.

The `ldflags` default value is `[]`.

> 💡 **Note:** Even though the configuration section is similar to the
[GoReleaser `builds` section](https://goreleaser.com/customization/build/),
only the `env`, `flags` and `ldflags` fields are currently supported. Also, the
templating support is currently limited to using environment variables only.
### Templating support

The `ko` builds supports templating of `flags` and `ldflags`, similar to the
[GoReleaser `builds` section](https://goreleaser.com/customization/build/).

The table below lists the supported template parameters.

| Template param | Description |
|-----------------------|---------------------------------------------------------------------------------------|
| `Env` | Map of system environment variables from `os.Environ` |
| `Date` | The UTC build date in RFC 3339 format |
| `Timestamp` | The UTC build date as Unix epoc seconds |
| `Git.Branch` | The current git branch |
| `Git.Tag` | The current git tag |
| `Git.ShortCommit` | The git commit short hash |
| `Git.FullCommit` | The git commit full hash |
| `Git.CommitDate` | The UTC commit date in RFC 3339 format |
| `Git.CommitTimestamp` | The UTC commit date in Unix format |
| `Git.URL` | The git remote url |
| `Git.Summary` | The git summary, e.g. `v1.0.0-10-g34f56g3` |
| `Git.TagSubject` | The annotated tag message subject, or the message subject of the commit it points out |
| `Git.TagContents` | The annotated tag message, or the message of the commit it points out |
| `Git.TagBody` | The annotated tag message's body, or the message's body of the commit it points out. |
| `Git.IsDirty` | Whether or not current git state is dirty |
| `Git.IsClean` | Whether or not current git state is clean. |
| `Git.TreeState` | Either `clean` or `dirty` |

### Setting default platforms

Expand Down
62 changes: 43 additions & 19 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"strconv"
"strings"
"text/template"
"time"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
Expand All @@ -40,6 +41,7 @@ import (
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/ko/internal/sbom"
"github.com/google/ko/pkg/caps"
"github.com/google/ko/pkg/internal/git"
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/v2/pkg/oci"
ocimutate "github.com/sigstore/cosign/v2/pkg/oci/mutate"
Expand All @@ -56,18 +58,24 @@ const (

defaultGoBin = "go" // defaults to first go binary found in PATH
goBinPathEnv = "KO_GO_PATH" // env lookup for optional relative or full go binary path

envTemplateKey = "Env"
gitTemplateKey = "Git"
dateTemplateKey = "Date"
timestampTemplateKey = "Timestamp"
)

// GetBase takes an importpath and returns a base image reference and base image (or index).
type GetBase func(context.Context, string) (name.Reference, Result, error)

// buildContext provides parameters for a builder function.
type buildContext struct {
ip string
dir string
env []string
platform v1.Platform
config Config
creationTime v1.Time
ip string
dir string
env []string
platform v1.Platform
config Config
}

type builder func(context.Context, buildContext) (string, error)
Expand Down Expand Up @@ -264,7 +272,7 @@ func getGoBinary() string {
}

func build(ctx context.Context, buildCtx buildContext) (string, error) {
buildArgs, err := createBuildArgs(buildCtx.config)
buildArgs, err := createBuildArgs(ctx, buildCtx)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -721,7 +729,7 @@ func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer
return buf, walkRecursive(tw, root, chroot, creationTime, platform)
}

func createTemplateData() map[string]interface{} {
func createTemplateData(ctx context.Context, buildCtx buildContext) map[string]interface{} {
envVars := map[string]string{
"LDFLAGS": "",
}
Expand All @@ -730,8 +738,23 @@ func createTemplateData() map[string]interface{} {
envVars[kv[0]] = kv[1]
}

// Get the git information, if available.
info, err := git.GetInfo(ctx, buildCtx.dir)
if err != nil {
log.Printf("%v", err)
}

// Use the creation time as the build date, if provided.
date := buildCtx.creationTime.Time
if date.IsZero() {
date = time.Now()
}

return map[string]interface{}{
"Env": envVars,
envTemplateKey: envVars,
gitTemplateKey: info.TemplateValue(),
dateTemplateKey: date.Format(time.RFC3339),
timestampTemplateKey: date.UTC().Unix(),
}
}

Expand All @@ -754,22 +777,22 @@ func applyTemplating(list []string, data map[string]interface{}) ([]string, erro
return result, nil
}

func createBuildArgs(buildCfg Config) ([]string, error) {
func createBuildArgs(ctx context.Context, buildCtx buildContext) ([]string, error) {
var args []string

data := createTemplateData()
data := createTemplateData(ctx, buildCtx)

if len(buildCfg.Flags) > 0 {
flags, err := applyTemplating(buildCfg.Flags, data)
if len(buildCtx.config.Flags) > 0 {
flags, err := applyTemplating(buildCtx.config.Flags, data)
if err != nil {
return nil, err
}

args = append(args, flags...)
}

if len(buildCfg.Ldflags) > 0 {
ldflags, err := applyTemplating(buildCfg.Ldflags, data)
if len(buildCtx.config.Ldflags) > 0 {
ldflags, err := applyTemplating(buildCtx.config.Ldflags, data)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -850,11 +873,12 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
// Do the build into a temporary file.
config := g.configForImportPath(ref.Path())
file, err := g.build(ctx, buildContext{
ip: ref.Path(),
dir: g.dir,
env: g.env,
platform: *platform,
config: config,
creationTime: g.creationTime,
ip: ref.Path(),
dir: g.dir,
env: g.env,
platform: *platform,
config: config,
})
if err != nil {
return nil, fmt.Errorf("build: %w", err)
Expand Down
110 changes: 110 additions & 0 deletions pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/ko/pkg/internal/gittesting"
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/v2/pkg/oci"
)
Expand Down Expand Up @@ -313,6 +314,108 @@ func TestBuildEnv(t *testing.T) {
}
}

func TestCreateTemplateData(t *testing.T) {
t.Run("env", func(t *testing.T) {
t.Setenv("FOO", "bar")
params := createTemplateData(context.TODO(), buildContext{})
vars := params["Env"].(map[string]string)
if vars["FOO"] != "bar" {
t.Fatalf("vars[FOO]=%q, want %q", vars["FOO"], "bar")
}
})

t.Run("empty creation time", func(t *testing.T) {
params := createTemplateData(context.TODO(), buildContext{})

// Make sure the date was set to time.Now().
actualDateStr := params["Date"].(string)
actualDate, err := time.Parse(time.RFC3339, actualDateStr)
if err != nil {
t.Fatal(err)
}
if time.Since(actualDate) > time.Minute {
t.Fatalf("expected date to be now, but was %v", actualDate)
}

// Check the timestamp.
actualTimestampSec := params["Timestamp"].(int64)
actualTimestamp := time.Unix(actualTimestampSec, 0)
expectedTimestamp := actualDate.Truncate(time.Second)
if !actualTimestamp.Equal(expectedTimestamp) {
t.Fatalf("expected timestamp %v, but was %v",
expectedTimestamp, actualTimestamp)
}
})

t.Run("creation time", func(t *testing.T) {
// Create a reference time for use as a creation time.
expectedTime, err := time.Parse(time.RFC3339, "2012-11-01T22:08:41+00:00")
if err != nil {
t.Fatal(err)
}

params := createTemplateData(context.TODO(), buildContext{
creationTime: v1.Time{Time: expectedTime},
})

// Check the date.
actualDateStr := params["Date"].(string)
actualDate, err := time.Parse(time.RFC3339, actualDateStr)
if err != nil {
t.Fatal(err)
}
if !actualDate.Equal(expectedTime) {
t.Fatalf("expected date to be %v, but was %v", expectedTime, actualDate)
}

// Check the timestamp.
actualTimestampSec := params["Timestamp"].(int64)
actualTimestamp := time.Unix(actualTimestampSec, 0)
if !actualTimestamp.Equal(expectedTime) {
t.Fatalf("expected timestamp to be %v, but was %v", expectedTime, actualTimestamp)
}
})

t.Run("no git available", func(t *testing.T) {
dir := t.TempDir()
params := createTemplateData(context.TODO(), buildContext{dir: dir})
gitParams := params["Git"].(map[string]interface{})

requireEqual(t, "", gitParams["Branch"])
requireEqual(t, "", gitParams["Tag"])
requireEqual(t, "", gitParams["ShortCommit"])
requireEqual(t, "", gitParams["FullCommit"])
requireEqual(t, "", gitParams["URL"])
requireEqual(t, "", gitParams["Summary"])
requireEqual(t, "", gitParams["TagSubject"])
requireEqual(t, "", gitParams["TagContents"])
requireEqual(t, "", gitParams["TagBody"])
requireEqual(t, "clean", gitParams["TreeState"])
})

t.Run("git", func(t *testing.T) {
// Create a fake git structure under the test temp dir.
const fakeGitURL = "git@github.com:foo/bar.git"
dir := t.TempDir()
gittesting.GitInit(t, dir)
gittesting.GitRemoteAdd(t, dir, fakeGitURL)
gittesting.GitCommit(t, dir, "commit1")
gittesting.GitTag(t, dir, "v0.0.1")

params := createTemplateData(context.TODO(), buildContext{dir: dir})
gitParams := params["Git"].(map[string]interface{})

requireEqual(t, "main", gitParams["Branch"])
requireEqual(t, "v0.0.1", gitParams["Tag"])
requireEqual(t, fakeGitURL, gitParams["URL"])
requireEqual(t, "v0.0.1", gitParams["Summary"])
requireEqual(t, "commit1", gitParams["TagSubject"])
requireEqual(t, "commit1", gitParams["TagContents"])
requireEqual(t, "", gitParams["TagBody"])
requireEqual(t, "clean", gitParams["TreeState"])
})
}

func TestBuildConfig(t *testing.T) {
tests := []struct {
description string
Expand Down Expand Up @@ -1248,3 +1351,10 @@ func TestGoBuildConsistentMediaTypes(t *testing.T) {
})
}
}

func requireEqual(t *testing.T, expected any, actual any) {
t.Helper()
if diff := cmp.Diff(expected, actual); diff != "" {
t.Fatalf("%T differ (-got, +want): %s", expected, diff)
}
}
64 changes: 64 additions & 0 deletions pkg/internal/git/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2024 ko Build Authors All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// MIT License
//
// Copyright (c) 2016-2022 Carlos Alexandro Becker
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

package git

import (
"errors"
"fmt"
)

var (
// ErrNoTag happens if the underlying git repository doesn't contain any tags
// but no snapshot-release was requested.
ErrNoTag = errors.New("git doesn't contain any tags. Tag info will not be available")

// ErrNotRepository happens if you try to run ko against a folder
// which is not a git repository.
ErrNotRepository = errors.New("current folder is not a git repository. Git info will not be available")

// ErrNoGit happens when git is not present in PATH.
ErrNoGit = errors.New("git not present in PATH. Git info will not be available")
)

// ErrDirty happens when the repo has uncommitted/unstashed changes.
type ErrDirty struct {
status string
}

func (e ErrDirty) Error() string {
return fmt.Sprintf("git is in a dirty state\nPlease check in your pipeline what can be changing the following files:\n%v\n", e.status)
}
Loading

0 comments on commit bd46af2

Please sign in to comment.