Skip to content

Commit ac2cab7

Browse files
authored
Add struct and functionality for using user input (#1387)
* Add parsing of user input Signed-off-by: Razieh Behjati <[email protected]> * Make the exposed LoadBuildConfigFromFile a method on DockerBuildConfig Signed-off-by: Razieh Behjati <[email protected]> Signed-off-by: Razieh Behjati <[email protected]>
1 parent 25fa0f3 commit ac2cab7

File tree

6 files changed

+308
-31
lines changed

6 files changed

+308
-31
lines changed

internal/builders/docker/commands.go

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,45 +27,26 @@ import (
2727
"github.com/spf13/cobra"
2828
)
2929

30-
// InputOptions are the common options for the dry run and build command.
31-
type InputOptions struct {
32-
BuildConfigPath string
33-
SourceRepo string
34-
GitCommitHash string
35-
BuilderImage string
36-
}
37-
38-
// AddFlags adds input flags to the given command.
39-
func (o *InputOptions) AddFlags(cmd *cobra.Command) {
40-
cmd.Flags().StringVarP(&o.BuildConfigPath, "build-config-path", "c", "",
41-
"Required - Path to a toml file containing the build configs.")
42-
43-
cmd.Flags().StringVarP(&o.SourceRepo, "source-repo", "s", "",
44-
"Required - URL of the source repo.")
45-
46-
cmd.Flags().StringVarP(&o.GitCommitHash, "git-commit-hash", "g", "",
47-
"Required - SHA1 Git commit digest of the revision of the source code to build the artefact from.")
48-
49-
cmd.Flags().StringVarP(&o.BuilderImage, "builder-image", "b", "",
50-
"Required - URL indicating the Docker builder image, including a URI and image digest.")
51-
}
52-
5330
// DryRunCmd validates the input flags, generates a BuildDefinition from them.
5431
func DryRunCmd(check func(error)) *cobra.Command {
55-
o := &InputOptions{}
32+
io := &pkg.InputOptions{}
5633
var buildDefinitionPath string
5734

5835
cmd := &cobra.Command{
5936
Use: "dry-run [FLAGS]",
6037
Short: "Generates and stores a JSON-formatted BuildDefinition based on the input arguments.",
6138
Run: func(cmd *cobra.Command, args []string) {
62-
// TODO(#1191): Parse the input arguments into an instance of BuildDefinition.
39+
config, err := pkg.NewDockerBuildConfig(io)
40+
check(err)
41+
log.Printf("The config is: %v\n", config)
42+
43+
// TODO(#1191): Create an instance of BuildDefinition from config.
6344
bd := &pkg.BuildDefinition{}
6445
check(writeBuildDefinitionToFile(*bd, buildDefinitionPath))
6546
},
6647
}
6748

68-
o.AddFlags(cmd)
49+
io.AddFlags(cmd)
6950

7051
cmd.Flags().StringVarP(&buildDefinitionPath, "build-definition-path", "o", "",
7152
"Required - Path to store the generated BuildDefinition to.")
@@ -87,20 +68,24 @@ func writeBuildDefinitionToFile(bd pkg.BuildDefinition, path string) error {
8768

8869
// BuildCmd builds the artifacts using the input flags, and prints out their digests, or exists with an error.
8970
func BuildCmd(check func(error)) *cobra.Command {
90-
o := &InputOptions{}
71+
io := &pkg.InputOptions{}
9172

9273
cmd := &cobra.Command{
9374
Use: "build [FLAGS]",
9475
Short: "Builds the artifacts using the build config, source repo, and the builder image.",
9576
Run: func(cmd *cobra.Command, args []string) {
96-
// TODO(#1191): Set up build state and build the artifact.
77+
config, err := pkg.NewDockerBuildConfig(io)
78+
check(err)
79+
log.Printf("The config is: %v\n", config)
80+
81+
// TODO(#1191): Set up build state using config, and build the artifact.
9782
artifacts := "To be implemented"
9883
log.Printf("Generated artifacts are: %v\n", artifacts)
9984
// TODO(#1191): Write subjects to file.
10085
},
10186
}
10287

103-
o.AddFlags(cmd)
88+
io.AddFlags(cmd)
10489

10590
return cmd
10691
}

internal/builders/docker/pkg/common_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func Test_BuildDefinition(t *testing.T) {
5252
},
5353
}
5454

55-
if !cmp.Equal(got, want) {
56-
t.Errorf(cmp.Diff(got, want))
55+
if diff := cmp.Diff(got, want); diff != "" {
56+
t.Errorf(diff)
5757
}
5858
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright 2022 SLSA Authors
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+
// https://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+
package pkg
16+
17+
// This file contains functionality and structs for validating and
18+
// representing user inputs and configuration files.
19+
20+
import (
21+
"fmt"
22+
"net/url"
23+
"strings"
24+
25+
toml "github.com/pelletier/go-toml"
26+
"github.com/slsa-framework/slsa-github-generator/internal/utils"
27+
)
28+
29+
// BuildConfig is a collection of parameters to use for building the artifact.
30+
type BuildConfig struct {
31+
// TODO(#1191): Add env and options if needed.
32+
// Command to pass to `docker run`. The command is taken as an array
33+
// instead of a single string to avoid unnecessary parsing. See
34+
// https://docs.docker.com/engine/reference/builder/#cmd and
35+
// https://man7.org/linux/man-pages/man3/exec.3.html for more details.
36+
Command []string `toml:"command"`
37+
38+
// The path, relative to the root of the git repository, where the artifact
39+
// built by the `docker run` command is expected to be found.
40+
ArtifactPath string `toml:"artifact_path"`
41+
}
42+
43+
// Digest specifies a digest values, including the name of the hash function
44+
// that was used for computing the digest.
45+
type Digest struct {
46+
Alg string
47+
Value string
48+
}
49+
50+
// DockerImage fully specifies a docker image by a URI (e.g., including the
51+
// docker image name and registry), and its digest.
52+
type DockerImage struct {
53+
URI string
54+
Digest Digest
55+
}
56+
57+
// ToString returns the builder image in the form of NAME@ALG:VALUE.
58+
func (bi *DockerImage) ToString() string {
59+
return fmt.Sprintf("%s@%s:%s", bi.URI, bi.Digest.Alg, bi.Digest.Value)
60+
}
61+
62+
// DockerBuildConfig is a convenience class for holding validated user inputs.
63+
type DockerBuildConfig struct {
64+
SourceRepo string
65+
SourceDigest Digest
66+
BuilderImage DockerImage
67+
BuildConfigPath string
68+
}
69+
70+
// NewDockerBuildConfig validates the inputs and generates an instance of
71+
// DockerBuildConfig.
72+
func NewDockerBuildConfig(io *InputOptions) (*DockerBuildConfig, error) {
73+
if err := validateURI(io.SourceRepo); err != nil {
74+
return nil, err
75+
}
76+
77+
sourceRepoDigest, err := validateDigest(io.GitCommitHash)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
dockerImage, err := validateDockerImage(io.BuilderImage)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
if err = validatePath(io.BuildConfigPath); err != nil {
88+
return nil, fmt.Errorf("invalid build config path: %v", err)
89+
}
90+
91+
return &DockerBuildConfig{
92+
SourceRepo: io.SourceRepo,
93+
SourceDigest: *sourceRepoDigest,
94+
BuilderImage: *dockerImage,
95+
BuildConfigPath: io.BuildConfigPath,
96+
}, nil
97+
}
98+
99+
func validateURI(input string) error {
100+
_, err := url.Parse(input)
101+
if err != nil {
102+
return fmt.Errorf("could not parse string (%q) as URI: %v", input, err)
103+
}
104+
return nil
105+
}
106+
107+
func validateDigest(input string) (*Digest, error) {
108+
// We expect the input to be of the form ALG:VALUE
109+
parts := strings.Split(input, ":")
110+
if len(parts) != 2 {
111+
return nil, fmt.Errorf("got %s, want ALG:VALUE format", input)
112+
}
113+
digest := Digest{
114+
Alg: parts[0],
115+
Value: parts[1],
116+
}
117+
return &digest, nil
118+
}
119+
120+
func validateDockerImage(image string) (*DockerImage, error) {
121+
imageParts := strings.Split(image, "@")
122+
if len(imageParts) != 2 {
123+
return nil, fmt.Errorf("got %s, want NAME@DIGEST format", image)
124+
}
125+
126+
if err := validateURI(imageParts[0]); err != nil {
127+
return nil, fmt.Errorf("docker image name (%q) is not a valid URI: %v", imageParts[0], err)
128+
}
129+
130+
digest, err := validateDigest(imageParts[1])
131+
if err != nil {
132+
return nil, fmt.Errorf("docker image digest (%q) is malformed: %v", imageParts[1], err)
133+
}
134+
135+
dockerImage := DockerImage{
136+
URI: imageParts[0],
137+
Digest: *digest,
138+
}
139+
140+
return &dockerImage, nil
141+
}
142+
143+
func validatePath(path string) error {
144+
err := utils.PathIsUnderCurrentDirectory(path)
145+
if err != nil {
146+
return fmt.Errorf("path (%q) is not in the current directory", path)
147+
}
148+
return nil
149+
}
150+
151+
// ToMap returns this instance as a mapping between the algorithm and value.
152+
func (d *Digest) ToMap() map[string]string {
153+
return map[string]string{d.Alg: d.Value}
154+
}
155+
156+
// LoadBuildConfigFromFile loads build configuration from a toml file specified
157+
// by the BuildConfigPath of this DockerBuildConfig. An instance of BuildConfig
158+
// is returned on success.
159+
func (dbc *DockerBuildConfig) LoadBuildConfigFromFile() (*BuildConfig, error) {
160+
return loadBuildConfigFromFile(dbc.BuildConfigPath)
161+
}
162+
163+
// loadBuildConfigFromFile does not validate the input path, and is therefore
164+
// not exposed. The corresponding method LoadBuildConfigFromFile must be called
165+
// on an instance of DockerBuildConfig which has a validated BuildConfigPath.
166+
func loadBuildConfigFromFile(path string) (*BuildConfig, error) {
167+
tomlTree, err := toml.LoadFile(path)
168+
if err != nil {
169+
return nil, fmt.Errorf("couldn't load toml file: %v", err)
170+
}
171+
172+
config := BuildConfig{}
173+
if err := tomlTree.Unmarshal(&config); err != nil {
174+
return nil, fmt.Errorf("couldn't ubmarshal toml file: %v", err)
175+
}
176+
177+
return &config, nil
178+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2022 SLSA Authors
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+
// https://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+
package pkg
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
)
22+
23+
func Test_LoadBuildConfigFromFile(t *testing.T) {
24+
got, err := loadBuildConfigFromFile("../testdata/config.toml")
25+
if err != nil {
26+
t.Fatalf("couldn't load config file: %v", err)
27+
}
28+
29+
want := BuildConfig{
30+
Command: []string{"cp", "internal/builders/docker/testdata/config.toml", "config.toml"},
31+
ArtifactPath: "config.toml",
32+
}
33+
34+
if diff := cmp.Diff(*got, want); diff != "" {
35+
t.Errorf(diff)
36+
}
37+
}
38+
39+
func Test_NewDockerBuildConfig(t *testing.T) {
40+
io := &InputOptions{
41+
BuildConfigPath: "testdata/config.toml",
42+
SourceRepo: "https://github.com/project-oak/transparent-release",
43+
GitCommitHash: "sha1:9b5f98310dbbad675834474fa68c37d880687cb9",
44+
BuilderImage: "bash@sha256:9e2ba52487d945504d250de186cb4fe2e3ba023ed2921dd6ac8b97ed43e76af9",
45+
}
46+
got, err := NewDockerBuildConfig(io)
47+
if err != nil {
48+
t.Fatalf("invalid inputs: %v", err)
49+
}
50+
51+
want := DockerBuildConfig{
52+
SourceRepo: io.SourceRepo,
53+
SourceDigest: Digest{
54+
Alg: "sha1",
55+
Value: "9b5f98310dbbad675834474fa68c37d880687cb9",
56+
},
57+
BuilderImage: DockerImage{
58+
URI: "bash",
59+
Digest: Digest{
60+
Alg: "sha256",
61+
Value: "9e2ba52487d945504d250de186cb4fe2e3ba023ed2921dd6ac8b97ed43e76af9",
62+
},
63+
},
64+
BuildConfigPath: io.BuildConfigPath,
65+
}
66+
67+
if diff := cmp.Diff(*got, want); diff != "" {
68+
t.Errorf(diff)
69+
}
70+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2022 SLSA Authors
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+
// https://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+
package pkg
16+
17+
import "github.com/spf13/cobra"
18+
19+
// InputOptions are the common options for the dry run and build command.
20+
type InputOptions struct {
21+
BuildConfigPath string
22+
SourceRepo string
23+
GitCommitHash string
24+
BuilderImage string
25+
}
26+
27+
// AddFlags adds input flags to the given command.
28+
func (io *InputOptions) AddFlags(cmd *cobra.Command) {
29+
cmd.Flags().StringVarP(&io.BuildConfigPath, "build-config-path", "c", "",
30+
"Required - Path to a toml file containing the build configs.")
31+
32+
cmd.Flags().StringVarP(&io.SourceRepo, "source-repo", "s", "",
33+
"Required - URL of the source repo.")
34+
35+
cmd.Flags().StringVarP(&io.GitCommitHash, "git-commit-hash", "g", "",
36+
"Required - SHA1 Git commit digest of the revision of the source code to build the artefact from.")
37+
38+
cmd.Flags().StringVarP(&io.BuilderImage, "builder-image", "b", "",
39+
"Required - URL indicating the Docker builder image, including a URI and image digest.")
40+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Simple command for generating a file.
2+
command = ["cp", "internal/builders/docker/testdata/config.toml", "config.toml"]
3+
# Path to the file generated by the command above.
4+
artifact_path = "config.toml"

0 commit comments

Comments
 (0)