Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add template params for git #1307

Merged
merged 1 commit into from
May 15, 2024
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
25 changes: 21 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,27 @@ 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.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
57 changes: 38 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 @@ -63,11 +65,12 @@ 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 +267,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 +724,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 +733,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,
"Env": envVars,
"Git": info.TemplateValue(),
"Date": date.Format(time.RFC3339),
"Timestamp": date.UTC().Unix(),
}
}

Expand All @@ -754,22 +772,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 +868,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
100 changes: 100 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,98 @@ 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, "clean", gitParams["TreeState"])
})

t.Run("git", func(t *testing.T) {
// Create a fake git structure under the test temp dir.
const fakeGitURL = "[email protected]: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, "clean", gitParams["TreeState"])
})
}

func TestBuildConfig(t *testing.T) {
tests := []struct {
description string
Expand Down Expand Up @@ -1248,3 +1341,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
Loading