Skip to content

Commit eda6b0d

Browse files
committed
feat: Add template params for git
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 #493 Signed-off-by: Nathan Mittler <[email protected]>
1 parent d432560 commit eda6b0d

File tree

9 files changed

+1025
-23
lines changed

9 files changed

+1025
-23
lines changed

docs/configuration.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,32 @@ of the `ko` process.
7777

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

80-
> 💡 **Note:** Even though the configuration section is similar to the
81-
[GoReleaser `builds` section](https://goreleaser.com/customization/build/),
82-
only the `env`, `flags` and `ldflags` fields are currently supported. Also, the
83-
templating support is currently limited to using environment variables only.
80+
### Templating support
81+
82+
The `ko` builds supports templating of `flags` and `ldflags`, similar to the
83+
[GoReleaser `builds` section](https://goreleaser.com/customization/build/).
84+
85+
The table below lists the supported template parameters.
86+
87+
| Template param | Description |
88+
|-----------------------|---------------------------------------------------------------------------------------|
89+
| `Env` | Map of system environment variables from `os.Environ` |
90+
| `Date` | The UTC build date in RFC 3339 format |
91+
| `Timestamp` | The UTC build date as Unix epoc seconds |
92+
| `Git.Branch` | The current git branch |
93+
| `Git.Tag` | The current git tag |
94+
| `Git.ShortCommit` | The git commit short hash |
95+
| `Git.FullCommit` | The git commit full hash |
96+
| `Git.CommitDate` | The UTC commit date in RFC 3339 format |
97+
| `Git.CommitTimestamp` | The UTC commit date in Unix format |
98+
| `Git.URL` | The git remote url |
99+
| `Git.Summary` | The git summary, e.g. `v1.0.0-10-g34f56g3` |
100+
| `Git.TagSubject` | The annotated tag message subject, or the message subject of the commit it points out |
101+
| `Git.TagContents` | The annotated tag message, or the message of the commit it points out |
102+
| `Git.TagBody` | The annotated tag message's body, or the message's body of the commit it points out. |
103+
| `Git.IsDirty` | Whether or not current git state is dirty |
104+
| `Git.IsClean` | Whether or not current git state is clean. |
105+
| `Git.TreeState` | Either `clean` or `dirty` |
84106

85107
### Setting default platforms
86108

pkg/build/gobuild.go

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"strconv"
3232
"strings"
3333
"text/template"
34+
"time"
3435

3536
"github.com/google/go-containerregistry/pkg/name"
3637
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -40,6 +41,7 @@ import (
4041
"github.com/google/go-containerregistry/pkg/v1/types"
4142
"github.com/google/ko/internal/sbom"
4243
"github.com/google/ko/pkg/caps"
44+
"github.com/google/ko/pkg/internal/git"
4345
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
4446
"github.com/sigstore/cosign/v2/pkg/oci"
4547
ocimutate "github.com/sigstore/cosign/v2/pkg/oci/mutate"
@@ -63,11 +65,12 @@ type GetBase func(context.Context, string) (name.Reference, Result, error)
6365

6466
// buildContext provides parameters for a builder function.
6567
type buildContext struct {
66-
ip string
67-
dir string
68-
env []string
69-
platform v1.Platform
70-
config Config
68+
creationTime v1.Time
69+
ip string
70+
dir string
71+
env []string
72+
platform v1.Platform
73+
config Config
7174
}
7275

7376
type builder func(context.Context, buildContext) (string, error)
@@ -264,7 +267,7 @@ func getGoBinary() string {
264267
}
265268

266269
func build(ctx context.Context, buildCtx buildContext) (string, error) {
267-
buildArgs, err := createBuildArgs(buildCtx.config)
270+
buildArgs, err := createBuildArgs(ctx, buildCtx)
268271
if err != nil {
269272
return "", err
270273
}
@@ -721,7 +724,7 @@ func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer
721724
return buf, walkRecursive(tw, root, chroot, creationTime, platform)
722725
}
723726

724-
func createTemplateData() map[string]interface{} {
727+
func createTemplateData(ctx context.Context, buildCtx buildContext) map[string]interface{} {
725728
envVars := map[string]string{
726729
"LDFLAGS": "",
727730
}
@@ -730,8 +733,23 @@ func createTemplateData() map[string]interface{} {
730733
envVars[kv[0]] = kv[1]
731734
}
732735

736+
// Get the git information, if available.
737+
info, err := git.GetInfo(ctx, buildCtx.dir)
738+
if err != nil {
739+
log.Printf("%v", err)
740+
}
741+
742+
// Use the creation time as the build date, if provided.
743+
date := buildCtx.creationTime.Time
744+
if date.IsZero() {
745+
date = time.Now()
746+
}
747+
733748
return map[string]interface{}{
734-
"Env": envVars,
749+
"Env": envVars,
750+
"Git": info.TemplateValue(),
751+
"Date": date.Format(time.RFC3339),
752+
"Timestamp": date.UTC().Unix(),
735753
}
736754
}
737755

@@ -754,22 +772,22 @@ func applyTemplating(list []string, data map[string]interface{}) ([]string, erro
754772
return result, nil
755773
}
756774

757-
func createBuildArgs(buildCfg Config) ([]string, error) {
775+
func createBuildArgs(ctx context.Context, buildCtx buildContext) ([]string, error) {
758776
var args []string
759777

760-
data := createTemplateData()
778+
data := createTemplateData(ctx, buildCtx)
761779

762-
if len(buildCfg.Flags) > 0 {
763-
flags, err := applyTemplating(buildCfg.Flags, data)
780+
if len(buildCtx.config.Flags) > 0 {
781+
flags, err := applyTemplating(buildCtx.config.Flags, data)
764782
if err != nil {
765783
return nil, err
766784
}
767785

768786
args = append(args, flags...)
769787
}
770788

771-
if len(buildCfg.Ldflags) > 0 {
772-
ldflags, err := applyTemplating(buildCfg.Ldflags, data)
789+
if len(buildCtx.config.Ldflags) > 0 {
790+
ldflags, err := applyTemplating(buildCtx.config.Ldflags, data)
773791
if err != nil {
774792
return nil, err
775793
}
@@ -850,11 +868,12 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
850868
// Do the build into a temporary file.
851869
config := g.configForImportPath(ref.Path())
852870
file, err := g.build(ctx, buildContext{
853-
ip: ref.Path(),
854-
dir: g.dir,
855-
env: g.env,
856-
platform: *platform,
857-
config: config,
871+
creationTime: g.creationTime,
872+
ip: ref.Path(),
873+
dir: g.dir,
874+
env: g.env,
875+
platform: *platform,
876+
config: config,
858877
})
859878
if err != nil {
860879
return nil, fmt.Errorf("build: %w", err)

pkg/build/gobuild_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/google/go-containerregistry/pkg/v1/mutate"
3838
"github.com/google/go-containerregistry/pkg/v1/random"
3939
"github.com/google/go-containerregistry/pkg/v1/types"
40+
"github.com/google/ko/pkg/internal/gittesting"
4041
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
4142
"github.com/sigstore/cosign/v2/pkg/oci"
4243
)
@@ -313,6 +314,98 @@ func TestBuildEnv(t *testing.T) {
313314
}
314315
}
315316

317+
func TestCreateTemplateData(t *testing.T) {
318+
t.Run("env", func(t *testing.T) {
319+
t.Setenv("FOO", "bar")
320+
params := createTemplateData(context.TODO(), buildContext{})
321+
vars := params["Env"].(map[string]string)
322+
if vars["FOO"] != "bar" {
323+
t.Fatalf("vars[FOO]=%q, want %q", vars["FOO"], "bar")
324+
}
325+
})
326+
327+
t.Run("empty creation time", func(t *testing.T) {
328+
params := createTemplateData(context.TODO(), buildContext{})
329+
330+
// Make sure the date was set to time.Now().
331+
actualDateStr := params["Date"].(string)
332+
actualDate, err := time.Parse(time.RFC3339, actualDateStr)
333+
if err != nil {
334+
t.Fatal(err)
335+
}
336+
if time.Since(actualDate) > time.Minute {
337+
t.Fatalf("expected date to be now, but was %v", actualDate)
338+
}
339+
340+
// Check the timestamp.
341+
actualTimestampSec := params["Timestamp"].(int64)
342+
actualTimestamp := time.Unix(actualTimestampSec, 0)
343+
expectedTimestamp := actualDate.Truncate(time.Second)
344+
if !actualTimestamp.Equal(expectedTimestamp) {
345+
t.Fatalf("expected timestamp %v, but was %v",
346+
expectedTimestamp, actualTimestamp)
347+
}
348+
})
349+
350+
t.Run("creation time", func(t *testing.T) {
351+
// Create a reference time for use as a creation time.
352+
expectedTime, err := time.Parse(time.RFC3339, "2012-11-01T22:08:41+00:00")
353+
if err != nil {
354+
t.Fatal(err)
355+
}
356+
357+
params := createTemplateData(context.TODO(), buildContext{
358+
creationTime: v1.Time{Time: expectedTime},
359+
})
360+
361+
// Check the date.
362+
actualDateStr := params["Date"].(string)
363+
actualDate, err := time.Parse(time.RFC3339, actualDateStr)
364+
if err != nil {
365+
t.Fatal(err)
366+
}
367+
if !actualDate.Equal(expectedTime) {
368+
t.Fatalf("expected date to be %v, but was %v", expectedTime, actualDate)
369+
}
370+
371+
// Check the timestamp.
372+
actualTimestampSec := params["Timestamp"].(int64)
373+
actualTimestamp := time.Unix(actualTimestampSec, 0)
374+
if !actualTimestamp.Equal(expectedTime) {
375+
t.Fatalf("expected timestamp to be %v, but was %v", expectedTime, actualTimestamp)
376+
}
377+
})
378+
379+
t.Run("no git available", func(t *testing.T) {
380+
dir := t.TempDir()
381+
params := createTemplateData(context.TODO(), buildContext{dir: dir})
382+
gitParams := params["Git"].(map[string]interface{})
383+
384+
requireEqual(t, "", gitParams["Branch"])
385+
requireEqual(t, "", gitParams["Tag"])
386+
requireEqual(t, "", gitParams["ShortCommit"])
387+
requireEqual(t, "", gitParams["FullCommit"])
388+
requireEqual(t, "clean", gitParams["TreeState"])
389+
})
390+
391+
t.Run("git", func(t *testing.T) {
392+
// Create a fake git structure under the test temp dir.
393+
const fakeGitURL = "[email protected]:foo/bar.git"
394+
dir := t.TempDir()
395+
gittesting.GitInit(t, dir)
396+
gittesting.GitRemoteAdd(t, dir, fakeGitURL)
397+
gittesting.GitCommit(t, dir, "commit1")
398+
gittesting.GitTag(t, dir, "v0.0.1")
399+
400+
params := createTemplateData(context.TODO(), buildContext{dir: dir})
401+
gitParams := params["Git"].(map[string]interface{})
402+
403+
requireEqual(t, "main", gitParams["Branch"])
404+
requireEqual(t, "v0.0.1", gitParams["Tag"])
405+
requireEqual(t, "clean", gitParams["TreeState"])
406+
})
407+
}
408+
316409
func TestBuildConfig(t *testing.T) {
317410
tests := []struct {
318411
description string
@@ -1248,3 +1341,10 @@ func TestGoBuildConsistentMediaTypes(t *testing.T) {
12481341
})
12491342
}
12501343
}
1344+
1345+
func requireEqual(t *testing.T, expected any, actual any) {
1346+
t.Helper()
1347+
if diff := cmp.Diff(expected, actual); diff != "" {
1348+
t.Fatalf("%T differ (-got, +want): %s", expected, diff)
1349+
}
1350+
}

pkg/internal/git/errors.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2024 ko Build Authors All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// MIT License
16+
//
17+
// Copyright (c) 2016-2022 Carlos Alexandro Becker
18+
//
19+
// Permission is hereby granted, free of charge, to any person obtaining a copy
20+
// of this software and associated documentation files (the "Software"), to deal
21+
// in the Software without restriction, including without limitation the rights
22+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23+
// copies of the Software, and to permit persons to whom the Software is
24+
// furnished to do so, subject to the following conditions:
25+
//
26+
// The above copyright notice and this permission notice shall be included in all
27+
// copies or substantial portions of the Software.
28+
//
29+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35+
// SOFTWARE.
36+
37+
package git
38+
39+
import (
40+
"errors"
41+
"fmt"
42+
)
43+
44+
var (
45+
// ErrNoTag happens if the underlying git repository doesn't contain any tags
46+
// but no snapshot-release was requested.
47+
ErrNoTag = errors.New("git doesn't contain any tags. Tag info will not be available")
48+
49+
// ErrNotRepository happens if you try to run ko against a folder
50+
// which is not a git repository.
51+
ErrNotRepository = errors.New("current folder is not a git repository. Git info will not be available")
52+
53+
// ErrNoGit happens when git is not present in PATH.
54+
ErrNoGit = errors.New("git not present in PATH. Git info will not be available")
55+
)
56+
57+
// ErrDirty happens when the repo has uncommitted/unstashed changes.
58+
type ErrDirty struct {
59+
status string
60+
}
61+
62+
func (e ErrDirty) Error() string {
63+
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)
64+
}

0 commit comments

Comments
 (0)