Skip to content

Commit

Permalink
kind/feat: surface artifacts through result sidecar container
Browse files Browse the repository at this point in the history
  • Loading branch information
ericzzzzzzz committed Apr 30, 2024
1 parent e556bc7 commit 1b04d6f
Show file tree
Hide file tree
Showing 14 changed files with 1,122 additions and 203 deletions.
20 changes: 17 additions & 3 deletions cmd/sidecarlogresults/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,19 @@ func main() {
var resultsDir string
var resultNames string
var stepResultsStr string
var stepNames string

flag.StringVar(&resultsDir, "results-dir", pipeline.DefaultResultPath, "Path to the results directory. Default is /tekton/results")
flag.StringVar(&resultNames, "result-names", "", "comma separated result names to expect from the steps running in the pod. eg. foo,bar,baz")
flag.StringVar(&stepResultsStr, "step-results", "", "json containing a map of step Name as key and list of result Names. eg. {\"stepName\":[\"foo\",\"bar\",\"baz\"]}")
flag.StringVar(&stepNames, "step-names", "", "comma separated step names. eg. foo,bar,baz")
flag.Parse()
if resultNames == "" {
log.Fatal("result-names were not provided")

var expectedResults []string
// strings.Split returns [""] instead of [] for empty string, we don't want pass [""] to other methods.
if len(resultNames) > 0 {
expectedResults = strings.Split(resultNames, ",")
}
expectedResults := strings.Split(resultNames, ",")
expectedStepResults := map[string][]string{}
if err := json.Unmarshal([]byte(stepResultsStr), &expectedStepResults); err != nil {
log.Fatal(err)
Expand All @@ -48,4 +53,13 @@ func main() {
if err != nil {
log.Fatal(err)
}

var names []string
if len(stepNames) > 0 {
names = strings.Split(stepNames, ",")
}
err = sidecarlogresults.LookForArtifacts(os.Stdout, names, pod.RunDir)
if err != nil {
log.Fatal(err)
}
}
4 changes: 2 additions & 2 deletions examples/v1/taskruns/alpha/produce-consume-artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ spec:
"name":"input-artifacts",
"values":[
{
"uri":"git:jjjsss",
"uri":"pkg:example.github.com/inputs",
"digest":{
"sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"
}
Expand All @@ -30,7 +30,7 @@ spec:
"name":"image",
"values":[
{
"uri":"pkg:balba",
"uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
"digest":{
"sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48",
"sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2"
Expand Down
58 changes: 56 additions & 2 deletions internal/sidecarlogresults/sidecarlogresults.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"strings"

"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/result"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
Expand All @@ -36,12 +38,15 @@ import (

// ErrSizeExceeded indicates that the result exceeded its maximum allowed size
var ErrSizeExceeded = errors.New("results size exceeds configured limit")
var stepDir = pipeline.StepsDir

type SidecarLogResultType string

const (
taskResultType SidecarLogResultType = "task"
stepResultType SidecarLogResultType = "step"
taskResultType SidecarLogResultType = "task"
stepResultType SidecarLogResultType = "step"

stepArtifactType SidecarLogResultType = "stepArtifact"
sidecarResultNameSeparator string = "."
)

Expand Down Expand Up @@ -197,6 +202,37 @@ func LookForResults(w io.Writer, runDir string, resultsDir string, resultNames [
return nil
}

// LookForArtifacts searches for and processes artifacts within a specified run directory.
// It looks for "provenance.json" files within the "artifacts" subdirectory of each named step.
// If the provenance file exists, the function extracts artifact information, formats it into a
// JSON string, and encodes it for output alongside relevant metadata (step name, artifact type).
func LookForArtifacts(w io.Writer, names []string, runDir string) error {
if err := waitForStepsToFinish(runDir); err != nil {
return err
}

for _, name := range names {
p := filepath.Join(stepDir, name, "artifacts", "provenance.json")
if exist, err := fileExists(p); err != nil {
return err
} else if !exist {
continue
}
subRes, err := extractArtifactsFromFile(p)
if err != nil {
return err
}
values, err := json.Marshal(&subRes)
if err != nil {
return err
}
if err := encode(w, SidecarLogResult{Name: name, Value: string(values), Type: stepArtifactType}); err != nil {
return err
}
}
return nil
}

// GetResultsFromSidecarLogs extracts results from the logs of the results sidecar
func GetResultsFromSidecarLogs(ctx context.Context, clientset kubernetes.Interface, namespace string, name string, container string, podPhase corev1.PodPhase) ([]result.RunResult, error) {
sidecarLogResults := []result.RunResult{}
Expand Down Expand Up @@ -250,6 +286,8 @@ func parseResults(resultBytes []byte, maxResultLimit int) (result.RunResult, err
resultType = result.TaskRunResultType
case stepResultType:
resultType = result.StepResultType
case stepArtifactType:
resultType = result.StepArtifactsResultType
default:
return result.RunResult{}, fmt.Errorf("invalid sidecar result type %v. Must be %v or %v", res.Type, taskResultType, stepResultType)
}
Expand All @@ -260,3 +298,19 @@ func parseResults(resultBytes []byte, maxResultLimit int) (result.RunResult, err
}
return runResult, nil
}

func parseArtifacts(fileContent []byte) (v1.Artifacts, error) {
var as v1.Artifacts
if err := json.Unmarshal(fileContent, &as); err != nil {
return as, fmt.Errorf("invalid artifacts : %w", err)
}
return as, nil
}

func extractArtifactsFromFile(filename string) (v1.Artifacts, error) {
b, err := os.ReadFile(filename)
if err != nil {
return v1.Artifacts{}, err
}
return parseArtifacts(b)
}
179 changes: 173 additions & 6 deletions internal/sidecarlogresults/sidecarlogresults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/result"
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fakekubeclientset "k8s.io/client-go/kubernetes/fake"
)
Expand Down Expand Up @@ -397,7 +397,7 @@ func TestParseResults_Failure(t *testing.T) {
func TestGetResultsFromSidecarLogs(t *testing.T) {
for _, c := range []struct {
desc string
podPhase v1.PodPhase
podPhase corev1.PodPhase
wantError bool
}{{
desc: "pod pending to start",
Expand All @@ -411,7 +411,7 @@ func TestGetResultsFromSidecarLogs(t *testing.T) {
t.Run(c.desc, func(t *testing.T) {
ctx := context.Background()
clientset := fakekubeclientset.NewSimpleClientset()
pod := &v1.Pod{
pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
Expand All @@ -420,15 +420,15 @@ func TestGetResultsFromSidecarLogs(t *testing.T) {
Name: "pod",
Namespace: "foo",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "image",
},
},
},
Status: v1.PodStatus{
Status: corev1.PodStatus{
Phase: c.podPhase,
},
}
Expand Down Expand Up @@ -474,6 +474,165 @@ func TestExtractStepAndResultFromSidecarResultName_Error(t *testing.T) {
}
}

func TestLookForArtifacts(t *testing.T) {
base := basicArtifacts()
var modified = base.DeepCopy()
modified.Outputs[0].Name = "tests"
type Arg struct {
stepName string
artifacts *v1.Artifacts
customContent []byte
}
tests := []struct {
desc string
wantErr bool
args []Arg
expected []SidecarLogResult
}{
{
desc: "one step produces artifacts, read success",
args: []Arg{{stepName: "first", artifacts: &base}},
expected: []SidecarLogResult{{
Name: "first",
Type: stepArtifactType,
Value: mustJSON(&base),
}},
}, {
desc: "two step produce artifacts, read success",
args: []Arg{{stepName: "first", artifacts: &base}, {stepName: "second", artifacts: modified}},
expected: []SidecarLogResult{{
Name: "first",
Type: stepArtifactType,
Value: mustJSON(&base),
}, {
Name: "second",
Type: stepArtifactType,
Value: mustJSON(modified),
}},
},
{
desc: "one step produces artifacts, one step does not, read success",
args: []Arg{{stepName: "first", artifacts: &base}, {stepName: "second"}},
expected: []SidecarLogResult{{
Name: "first",
Type: stepArtifactType,
Value: mustJSON(&base),
}},
},
{
desc: "two step produces, one read success, one not, error out and result is not empty.",
args: []Arg{{stepName: "first", artifacts: &base}, {stepName: "second", artifacts: modified, customContent: []byte("this is to break json")}},
expected: []SidecarLogResult{{
Name: "first",
Type: stepArtifactType,
Value: mustJSON(&base),
}},
wantErr: true,
},
{
desc: "two step produces, first read fails, error out and result is empty.",
args: []Arg{{stepName: "first", artifacts: modified, customContent: []byte("this is to break json")}, {stepName: "second", artifacts: &base}},
expected: []SidecarLogResult{},
wantErr: true,
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
dir := t.TempDir()
curStepDir := stepDir
stepDir = dir
t.Cleanup(func() {
stepDir = curStepDir
})

var names []string
for _, arg := range tc.args {
names = append(names, arg.stepName)
if err := os.MkdirAll(filepath.Join(dir, arg.stepName, "artifacts"), os.ModePerm); err != nil {
t.Errorf("failed to create artifacts folder, err: %v", err)
}
if _, err := os.Create(filepath.Join(dir, arg.stepName, "out")); err != nil {
t.Errorf("failed to file, err: %v", err)
}
if arg.artifacts != nil {
if err := writeArtifacts(filepath.Join(dir, arg.stepName, "artifacts", "provenance.json"), arg.artifacts); err != nil {
t.Errorf("failed to write artifacts to provenance.json, err: %v", err)
}
}
if arg.customContent != nil {
if err := os.WriteFile(filepath.Join(dir, arg.stepName, "artifacts", "provenance.json"), arg.customContent, os.ModePerm); err != nil {
t.Errorf("failed to write customContent to provenance.json, err: %v", err)
}
}
}
var buf bytes.Buffer
err := LookForArtifacts(&buf, names, dir)
if (err != nil) != tc.wantErr {
t.Errorf("error checking failed, wantErr: %v, got: %v", tc.wantErr, err)
}
want := ""
for _, logResult := range tc.expected {
want += mustJSON(logResult) + "\n"
}
got := buf.String()

if d := cmp.Diff(want, got); d != "" {
t.Errorf(diff.PrintWantGot(d))
}
})
}
}

func writeArtifacts(path string, artifacts *v1.Artifacts) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
res := json.NewEncoder(f).Encode(artifacts)
return res
}

func basicArtifacts() v1.Artifacts {
data := `{
"inputs":[
{
"name":"inputs",
"values":[
{
"uri":"pkg:example.github.com/inputs",
"digest":{
"sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"
}
}
]
}
],
"outputs":[
{
"name":"image",
"values":[
{
"uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
"digest":{
"sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48",
"sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2"
}
}
]
}
]
}
`
var ars v1.Artifacts
err := json.Unmarshal([]byte(data), &ars)
if err != nil {
panic(err)
}
return ars
}

func createStepResult(t *testing.T, dir, stepName, resultName, resultValue string) {
t.Helper()
resultDir := filepath.Join(dir, stepName, "results")
Expand Down Expand Up @@ -507,3 +666,11 @@ func createRun(t *testing.T, dir string, causeErr bool) {
t.Fatal(err)
}
}

func mustJSON(data any) string {
marshal, err := json.Marshal(data)
if err != nil {
panic(err)
}
return string(marshal)
}

0 comments on commit 1b04d6f

Please sign in to comment.