Skip to content

Commit b4e9c57

Browse files
authored
feat(cli): automatically discover runner context (#518)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 9d8dfdf commit b4e9c57

18 files changed

+168
-103
lines changed

app/cli/internal/action/attestation_init.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,14 @@ func (action *AttestationInit) Run(ctx context.Context, contractRevision int) (s
8383
action.Logger.Debug().Msg("Retrieving attestation definition")
8484
client := pb.NewAttestationServiceClient(action.ActionsOpts.CPConnection)
8585
// get information of the workflow
86-
resp, err := client.GetContract(ctx, &pb.AttestationServiceGetContractRequest{ContractRevision: int32(contractRevision)})
86+
contractResp, err := client.GetContract(ctx, &pb.AttestationServiceGetContractRequest{ContractRevision: int32(contractRevision)})
8787
if err != nil {
8888
return "", err
8989
}
9090

91-
workflow := resp.GetResult().GetWorkflow()
92-
contractVersion := resp.Result.GetContract()
91+
workflow := contractResp.GetResult().GetWorkflow()
92+
contractVersion := contractResp.Result.GetContract()
93+
contract := contractResp.GetResult().GetContract().GetV1()
9394

9495
workflowMeta := &clientAPI.WorkflowMetadata{
9596
WorkflowId: workflow.GetId(),
@@ -101,12 +102,10 @@ func (action *AttestationInit) Run(ctx context.Context, contractRevision int) (s
101102

102103
action.Logger.Debug().Msg("workflow contract and metadata retrieved from the control plane")
103104

104-
// Check that the instantiation is happening in the right runner context
105-
// and extract the job URL
106-
runnerType := resp.Result.Contract.GetV1().Runner.GetType()
107-
runnerContext := crafter.NewRunner(runnerType)
108-
if !action.dryRun && !runnerContext.CheckEnv() {
109-
return "", ErrRunnerContextNotFound{runnerContext.String()}
105+
// Auto discover the runner context and enforce against the one in the contract if needed
106+
discoveredRunner, err := crafter.DiscoverAndEnforceRunner(contract.GetRunner().GetType(), action.dryRun, action.Logger)
107+
if err != nil {
108+
return "", ErrRunnerContextNotFound{err.Error()}
110109
}
111110

112111
// Identifier of this attestation instance
@@ -117,7 +116,7 @@ func (action *AttestationInit) Run(ctx context.Context, contractRevision int) (s
117116
runResp, err := client.Init(
118117
ctx,
119118
&pb.AttestationServiceInitRequest{
120-
JobUrl: runnerContext.RunURI(),
119+
JobUrl: discoveredRunner.RunURI(),
121120
ContractRevision: int32(contractRevision),
122121
},
123122
)
@@ -138,6 +137,7 @@ func (action *AttestationInit) Run(ctx context.Context, contractRevision int) (s
138137
WfInfo: workflowMeta, SchemaV1: contractVersion.GetV1(),
139138
DryRun: action.dryRun,
140139
AttestationID: attestationID,
140+
Runner: discoveredRunner,
141141
}
142142

143143
if err := action.c.Init(ctx, initOpts); err != nil {

internal/attestation/crafter/crafter.go

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type StateManager interface {
5858
type Crafter struct {
5959
logger *zerolog.Logger
6060
CraftingState *api.CraftingState
61-
Runner supportedRunner
61+
Runner SupportedRunner
6262
workingDir string
6363
stateManager StateManager
6464
// Authn is used to authenticate with the OCI registry
@@ -126,6 +126,7 @@ type InitOpts struct {
126126
DryRun bool
127127
// Identifier of the attestation state
128128
AttestationID string
129+
Runner SupportedRunner
129130
}
130131

131132
// Initialize the crafter with a remote or local schema
@@ -136,14 +137,7 @@ func (c *Crafter) Init(ctx context.Context, opts *InitOpts) error {
136137
return errors.New("workflow metadata is nil")
137138
}
138139

139-
// Check that the initialization is happening in the right environment
140-
runnerType := opts.SchemaV1.Runner.GetType()
141-
runnerContext := NewRunner(runnerType)
142-
if !opts.DryRun && !runnerContext.CheckEnv() {
143-
return fmt.Errorf("%w, expected %s", ErrRunnerContextNotFound, runnerType)
144-
}
145-
146-
return c.initCraftingStateFile(ctx, opts.AttestationID, opts.SchemaV1, opts.WfInfo, opts.DryRun, runnerType, runnerContext.RunURI())
140+
return c.initCraftingStateFile(ctx, opts.AttestationID, opts.SchemaV1, opts.WfInfo, opts.DryRun, opts.Runner.ID(), opts.Runner.RunURI())
147141
}
148142

149143
func (c *Crafter) AlreadyInitialized(ctx context.Context, stateID string) (bool, error) {
@@ -247,7 +241,7 @@ func (c *Crafter) LoadCraftingState(ctx context.Context, attestationID string) e
247241
}
248242

249243
// Set runner too
250-
runnerType := c.CraftingState.GetInputSchema().GetRunner().GetType()
244+
runnerType := c.CraftingState.GetAttestation().GetRunnerType()
251245
if runnerType.String() == "" {
252246
return errors.New("runner type not set in the crafting state")
253247
}
@@ -389,9 +383,9 @@ func (c *Crafter) ResolveEnvVars(ctx context.Context, attestationID string) erro
389383
}
390384

391385
// Runner specific environment variables
392-
c.logger.Debug().Str("runnerType", c.Runner.String()).Msg("loading runner specific env variables")
386+
c.logger.Debug().Str("runnerType", c.Runner.ID().String()).Msg("loading runner specific env variables")
393387
if !c.Runner.CheckEnv() {
394-
errorStr := fmt.Sprintf("couldn't detect the environment %q. Is the crafting process happening in the target env?", c.Runner.String())
388+
errorStr := fmt.Sprintf("couldn't detect the environment %q. Is the crafting process happening in the target env?", c.Runner.ID().String())
395389
return fmt.Errorf("%s - %w", errorStr, ErrRunnerContextNotFound)
396390
}
397391

@@ -400,7 +394,7 @@ func (c *Crafter) ResolveEnvVars(ctx context.Context, attestationID string) erro
400394
for index, envVarDef := range c.Runner.ListEnvVars() {
401395
varNames[index] = envVarDef.Name
402396
}
403-
c.logger.Debug().Str("runnerType", c.Runner.String()).Strs("variables", varNames).Msg("list of env variables to automatically extract")
397+
c.logger.Debug().Str("runnerType", c.Runner.ID().String()).Strs("variables", varNames).Msg("list of env variables to automatically extract")
404398

405399
outputEnvVars, errors := c.Runner.ResolveEnvVars()
406400
if len(errors) > 0 {

internal/attestation/crafter/crafter_test.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2727
"github.com/chainloop-dev/chainloop/internal/attestation/crafter"
2828
v1 "github.com/chainloop-dev/chainloop/internal/attestation/crafter/api/attestation/v1"
29+
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/runners"
2930
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/statemanager/filesystem"
3031
"github.com/go-git/go-git/v5"
3132
"github.com/go-git/go-git/v5/plumbing/object"
@@ -74,12 +75,6 @@ func (s *crafterSuite) TestInit() {
7475
workflowMetadata: nil,
7576
wantErr: true,
7677
},
77-
{
78-
name: "required github action environment, can't run",
79-
contractPath: "testdata/contracts/empty_github.yaml",
80-
workflowMetadata: s.workflowMetadata,
81-
wantErr: true,
82-
},
8378
{
8479
name: "required github action env (dry run)",
8580
contractPath: "testdata/contracts/empty_github.yaml",
@@ -102,9 +97,10 @@ func (s *crafterSuite) TestInit() {
10297
contract, err := crafter.LoadSchema(tc.contractPath)
10398
require.NoError(s.T(), err)
10499

100+
runner := crafter.NewRunner(contract.GetRunner().GetType())
105101
// Make sure that the tests context indicate that we are not in a CI
106102
// this makes the github action runner context to fail
107-
c, err := newInitializedCrafter(s.T(), tc.contractPath, tc.workflowMetadata, tc.dryRun, tc.workingDir)
103+
c, err := newInitializedCrafter(s.T(), tc.contractPath, tc.workflowMetadata, tc.dryRun, tc.workingDir, runner)
108104
if tc.wantErr {
109105
s.Error(err)
110106
return
@@ -149,12 +145,20 @@ func testingStateManager(t *testing.T, statePath string) crafter.StateManager {
149145
return stateManager
150146
}
151147

152-
func newInitializedCrafter(t *testing.T, contractPath string, wfMeta *v1.WorkflowMetadata, dryRun bool, workingDir string) (*testingCrafter, error) {
148+
func newInitializedCrafter(t *testing.T, contractPath string, wfMeta *v1.WorkflowMetadata,
149+
dryRun bool,
150+
workingDir string,
151+
runner crafter.SupportedRunner,
152+
) (*testingCrafter, error) {
153153
opts := []crafter.NewOpt{}
154154
if workingDir != "" {
155155
opts = append(opts, crafter.WithWorkingDirPath(workingDir))
156156
}
157157

158+
if runner == nil {
159+
runner = runners.NewGeneric()
160+
}
161+
158162
statePath := fmt.Sprintf("%s/attestation.json", t.TempDir())
159163
c, err := crafter.NewCrafter(testingStateManager(t, statePath), opts...)
160164
require.NoError(t, err)
@@ -163,7 +167,10 @@ func newInitializedCrafter(t *testing.T, contractPath string, wfMeta *v1.Workflo
163167
return nil, err
164168
}
165169

166-
if err = c.Init(context.Background(), &crafter.InitOpts{SchemaV1: contract, WfInfo: wfMeta, DryRun: dryRun, AttestationID: ""}); err != nil {
170+
if err = c.Init(context.Background(), &crafter.InitOpts{
171+
SchemaV1: contract, WfInfo: wfMeta, DryRun: dryRun,
172+
AttestationID: "",
173+
Runner: runner}); err != nil {
167174
return nil, err
168175
}
169176

@@ -293,27 +300,30 @@ func (s *crafterSuite) TestResolveEnvVars() {
293300

294301
for _, tc := range testCases {
295302
s.Run(tc.name, func() {
303+
var runner crafter.SupportedRunner = runners.NewGeneric()
296304
contract := "testdata/contracts/with_env_vars.yaml"
297305
if tc.inGithubEnv {
298306
s.T().Setenv("CI", "true")
299307
for k, v := range gitHubTestingEnvVars {
300308
s.T().Setenv(k, v)
301309
}
310+
runner = runners.NewGithubAction()
302311
} else if tc.inJenkinsEnv {
303312
contract = "testdata/contracts/jenkins_with_env_vars.yaml"
304313
s.T().Setenv("JOB_NAME", "some-job")
305314
s.T().Setenv("BUILD_URL", "http://some-url")
306315
s.T().Setenv("AGENT_WORKDIR", "/some/home/dir")
307316
s.T().Setenv("NODE_NAME", "some-node")
308317
s.T().Setenv("JENKINS_HOME", "/some/home/dir")
318+
runner = runners.NewJenkinsJob()
309319
}
310320

311321
// Customs env vars
312322
for k, v := range tc.envVars {
313323
s.T().Setenv(k, v)
314324
}
315325

316-
c, err := newInitializedCrafter(s.T(), contract, &v1.WorkflowMetadata{}, false, "")
326+
c, err := newInitializedCrafter(s.T(), contract, &v1.WorkflowMetadata{}, false, "", runner)
317327
require.NoError(s.T(), err)
318328

319329
err = c.ResolveEnvVars(context.Background(), "")

internal/attestation/crafter/runner.go

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ package crafter
1717

1818
import (
1919
"errors"
20+
"fmt"
2021

2122
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2223
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/runners"
24+
"github.com/rs/zerolog"
2325
)
2426

2527
var ErrRunnerContextNotFound = errors.New("the runner environment doesn't match the required runner type")
2628

27-
type supportedRunner interface {
29+
type SupportedRunner interface {
2830
// Whether the attestation is happening in this environment
2931
CheckEnv() bool
3032

@@ -34,27 +36,80 @@ type supportedRunner interface {
3436
// Return the list of env vars associated with this runner already resolved
3537
ResolveEnvVars() (map[string]string, []*error)
3638

37-
String() string
38-
3939
// uri to the running job/workload
4040
RunURI() string
41+
42+
// ID returns the runner type
43+
ID() schemaapi.CraftingSchema_Runner_RunnerType
44+
}
45+
46+
type RunnerM map[schemaapi.CraftingSchema_Runner_RunnerType]SupportedRunner
47+
48+
var RunnersMap = map[schemaapi.CraftingSchema_Runner_RunnerType]SupportedRunner{
49+
schemaapi.CraftingSchema_Runner_GITHUB_ACTION: runners.NewGithubAction(),
50+
schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: runners.NewGitlabPipeline(),
51+
schemaapi.CraftingSchema_Runner_AZURE_PIPELINE: runners.NewAzurePipeline(),
52+
schemaapi.CraftingSchema_Runner_JENKINS_JOB: runners.NewJenkinsJob(),
53+
schemaapi.CraftingSchema_Runner_CIRCLECI_BUILD: runners.NewCircleCIBuild(),
54+
schemaapi.CraftingSchema_Runner_DAGGER_PIPELINE: runners.NewDaggerPipeline(),
55+
}
56+
57+
// Load a specific runner
58+
func NewRunner(t schemaapi.CraftingSchema_Runner_RunnerType) SupportedRunner {
59+
if r, ok := RunnersMap[t]; ok {
60+
return r
61+
}
62+
63+
return runners.NewGeneric()
4164
}
4265

43-
func NewRunner(t schemaapi.CraftingSchema_Runner_RunnerType) supportedRunner {
44-
switch t {
45-
case schemaapi.CraftingSchema_Runner_GITHUB_ACTION:
46-
return runners.NewGithubAction()
47-
case schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE:
48-
return runners.NewGitlabPipeline()
49-
case schemaapi.CraftingSchema_Runner_AZURE_PIPELINE:
50-
return runners.NewAzurePipeline()
51-
case schemaapi.CraftingSchema_Runner_JENKINS_JOB:
52-
return runners.NewJenkinsJob()
53-
case schemaapi.CraftingSchema_Runner_CIRCLECI_BUILD:
54-
return runners.NewCircleCIBuild()
55-
case schemaapi.CraftingSchema_Runner_DAGGER_PIPELINE:
56-
return runners.NewDaggerPipeline()
57-
default:
66+
// Discover the runner environment
67+
// This method does a simple check to see which runner is available in the environment
68+
// by iterating over the different runners and performing duck-typing checks
69+
// If more than one runner is detected, we default to generic since its an incongruent result
70+
func discoverRunner(logger zerolog.Logger) SupportedRunner {
71+
detected := []SupportedRunner{}
72+
for _, r := range RunnersMap {
73+
if r.CheckEnv() {
74+
detected = append(detected, r)
75+
}
76+
}
77+
78+
// if we don't detect any runner or more than one, we default to generic
79+
if len(detected) == 0 {
80+
return runners.NewGeneric()
81+
}
82+
83+
if len(detected) > 1 {
84+
var detectedStr []string
85+
for _, d := range detected {
86+
detectedStr = append(detectedStr, d.ID().String())
87+
}
88+
89+
logger.Warn().Strs("detected", detectedStr).Msg("multiple runners detected, incongruent environment")
5890
return runners.NewGeneric()
5991
}
92+
93+
return detected[0]
94+
}
95+
96+
func DiscoverAndEnforceRunner(enforcedRunnerType schemaapi.CraftingSchema_Runner_RunnerType, dryRun bool, logger zerolog.Logger) (SupportedRunner, error) {
97+
discoveredRunner := discoverRunner(logger)
98+
99+
logger.Debug().
100+
Str("discovered", discoveredRunner.ID().String()).
101+
Str("enforced", enforcedRunnerType.String()).
102+
Msg("checking runner context")
103+
104+
// If the runner type is not specified and it's a dry run, we don't enforce it
105+
if enforcedRunnerType == schemaapi.CraftingSchema_Runner_RUNNER_TYPE_UNSPECIFIED || dryRun {
106+
return discoveredRunner, nil
107+
}
108+
109+
// Otherwise we enforce the runner type
110+
if enforcedRunnerType != discoveredRunner.ID() {
111+
return nil, fmt.Errorf("runner not found %s", enforcedRunnerType)
112+
}
113+
114+
return discoveredRunner, nil
60115
}

internal/attestation/crafter/runners/azurepipeline.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@ import (
1919
neturl "net/url"
2020
"os"
2121
"path"
22+
23+
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2224
)
2325

2426
type AzurePipeline struct{}
2527

26-
const AzurePipelineID = "azure-pipeline"
27-
2828
func NewAzurePipeline() *AzurePipeline {
2929
return &AzurePipeline{}
3030
}
3131

32+
func (r *AzurePipeline) ID() schemaapi.CraftingSchema_Runner_RunnerType {
33+
return schemaapi.CraftingSchema_Runner_AZURE_PIPELINE
34+
}
35+
3236
// Figure out if we are in a Azure Pipeline job or not
3337
func (r *AzurePipeline) CheckEnv() bool {
3438
for _, varName := range []string{"TF_BUILD", "BUILD_BUILDURI"} {
@@ -55,10 +59,6 @@ func (r *AzurePipeline) ListEnvVars() []*EnvVarDefinition {
5559
}
5660
}
5761

58-
func (r *AzurePipeline) String() string {
59-
return AzurePipelineID
60-
}
61-
6262
func (r *AzurePipeline) RunURI() (url string) {
6363
teamFoundationServerURI := os.Getenv("SYSTEM_TEAMFOUNDATIONSERVERURI")
6464
definitionName := os.Getenv("SYSTEM_TEAMPROJECT")

internal/attestation/crafter/runners/azurepipeline_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (s *azurePipelineSuite) TestRunURI() {
115115
}
116116

117117
func (s *azurePipelineSuite) TestRunnerName() {
118-
s.Equal("azure-pipeline", s.runner.String())
118+
s.Equal("AZURE_PIPELINE", s.runner.ID().String())
119119
}
120120

121121
// Run before each test

0 commit comments

Comments
 (0)