Skip to content
Draft
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
1 change: 1 addition & 0 deletions cmd/controlplane/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ func (o *controllerOptions) setupReconcilers(

if err := stages.NewRegularStageReconciler(
stagesReconcilerCfg,
credentialsDB,
health.NewAggregatingChecker(),
).SetupWithManager(
ctx,
Expand Down
59 changes: 59 additions & 0 deletions docs/docs/50-user-guide/60-reference-docs/40-expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,65 @@ config:

:::

### `repoCredentials(repoURL, type)`

The `repoCredentials()` function resolves repository credentials by repository
URL and credential type, returning them as an object whose fields are accessed
by name. It takes two required arguments:

- `repoURL` (Required): A string representing the URL of the repository whose
credentials should be resolved.

- `type` (Required): A string representing the credential type. Must be one of
`git`, `helm`, or `image`. The type is required because credentials are
indexed by type, and the same URL may resolve to different credentials
depending on it.

Unlike `secret()`, which returns the raw `Data` of a `Secret` selected by
_name_, `repoCredentials()` performs a lookup by repository URL through the same
credentials database used by built-in promotion steps like `git-clone`. It
returns the **resolved** credentials — not the raw contents of the underlying
`Secret`. This means it transparently handles credential schemes that yield
narrowly-scoped, short-lived credentials — such as GitHub App installation
tokens or a cloud provider's ambient (Pod identity) credentials — instead of
returning the raw material (e.g. an app ID and private key) from which those
credentials are derived.

Because the result is normalized, the set of available fields is **fixed** and
does not vary by credential type or provider. The returned object always exposes
all of the following fields:

| Field | Description |
|-------|-------------|
| `Username` | The username identifying the principal. For token-based credentials this is often an inconsequential placeholder. |
| `Password` | The password or token used to authenticate. **API keys and personal access tokens are surfaced here.** |
| `SSHPrivateKey` | The SSH private key, when applicable. **Deprecated as of v1.10.0** and slated for removal in v1.13.0. |

If no matching credentials are found, an object with all fields empty is
returned.
Comment on lines +500 to +501

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be the optimal behavior. It's common to use optional chaining and nil-coalescing in expressions, so returning nil is probably a more useful signal when no credentials are found.


For details on how each field is populated for a given credential type or
provider (e.g. GitHub App, ECR, or basic username/password), see the
[Managing Secrets](../50-security/30-managing-secrets.md#repository-credentials)
page.

Examples:

```yaml
config:
headers:
- name: Authorization
value: Bearer ${{ repoCredentials('https://github.com/example/repo.git', 'git').Password }}
```

:::note

`repoCredentials()` is available wherever Kargo's credentials database is wired:
within `Promotion` step expressions and within a `Stage`'s verification argument
expressions.

:::

### `warehouse(name)`

The `warehouse()` function returns a `FreightOrigin` object representing a
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/promotions/argocd_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func promotionSelectorsMatchApp(
}

promoCtx := promotion.NewContext(promo, stage)
evaluator := promotion.NewStepEvaluator(cl, nil)
evaluator := promotion.NewStepEvaluator(cl, nil, nil)

for i, step := range promo.Spec.Steps {
if int64(i) > promo.Status.CurrentStep {
Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/stages/regular_stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/akuity/kargo/pkg/conditions"
"github.com/akuity/kargo/pkg/controller"
argocdapi "github.com/akuity/kargo/pkg/controller/argocd/api/v1alpha1"
"github.com/akuity/kargo/pkg/credentials"
kargoEvent "github.com/akuity/kargo/pkg/event"
k8sevent "github.com/akuity/kargo/pkg/event/kubernetes"
exprfn "github.com/akuity/kargo/pkg/expressions/function"
Expand Down Expand Up @@ -77,6 +78,7 @@ func ReconcilerConfigFromEnv() ReconcilerConfig {
type RegularStageReconciler struct {
cfg ReconcilerConfig
client client.Client
credentialsDB credentials.Database
eventSender kargoEvent.Sender
healthChecker health.AggregatingChecker
shardPredicate controller.ResponsibleFor[kargoapi.Stage]
Expand All @@ -87,10 +89,12 @@ type RegularStageReconciler struct {
// NewRegularStageReconciler creates a new Stages reconciler.
func NewRegularStageReconciler(
cfg ReconcilerConfig,
credentialsDB credentials.Database,
healthChecker health.AggregatingChecker,
) *RegularStageReconciler {
return &RegularStageReconciler{
cfg: cfg,
credentialsDB: credentialsDB,
healthChecker: healthChecker,
shardPredicate: controller.ResponsibleFor[kargoapi.Stage]{
IsDefaultController: cfg.IsDefaultController,
Expand Down Expand Up @@ -1386,6 +1390,7 @@ func (r *RegularStageReconciler) startVerification(
exprfn.DataOperations(
ctx,
r.client,
r.credentialsDB,
gocache.New(gocache.NoExpiration, gocache.NoExpiration),
stage.Namespace,
),
Expand Down
136 changes: 135 additions & 1 deletion pkg/controller/stages/regular_stages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
rolloutsapi "github.com/akuity/kargo/api/stubs/rollouts/v1alpha1"
kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/pkg/conditions"
"github.com/akuity/kargo/pkg/credentials"
k8sevent "github.com/akuity/kargo/pkg/event/kubernetes"
"github.com/akuity/kargo/pkg/health"
"github.com/akuity/kargo/pkg/indexer"
Expand Down Expand Up @@ -3622,6 +3623,7 @@ func TestRegularStageReconciler_startVerification(t *testing.T) {
freightCol kargoapi.FreightCollection
req *kargoapi.VerificationRequest
objects []client.Object
credsDB credentials.Database
rolloutsDisabled bool
assertions func(*testing.T, client.Client, *kargoapi.VerificationInfo, error)
}{
Expand Down Expand Up @@ -3988,6 +3990,137 @@ func TestRegularStageReconciler_startVerification(t *testing.T) {
)
},
},
{
name: "resolves repoCredentials() in verification arguments",
stage: &kargoapi.Stage{
ObjectMeta: metav1.ObjectMeta{
Namespace: "fake-project",
Name: "test-stage",
},
Spec: kargoapi.StageSpec{
Verification: &kargoapi.Verification{
AnalysisTemplates: []kargoapi.AnalysisTemplateReference{
{Name: "test-template"},
},
Args: []kargoapi.AnalysisRunArgument{
{
Name: "token",
Value: "${{ repoCredentials(" +
"'https://github.com/example/repo.git', 'git'" +
").Password }}",
},
},
},
},
},
freightCol: kargoapi.FreightCollection{
ID: "test-collection",
Freight: map[string]kargoapi.FreightReference{
"warehouse": {Name: "test-freight"},
},
},
objects: []client.Object{
&kargoapi.Freight{
ObjectMeta: metav1.ObjectMeta{
Name: "test-freight",
Namespace: "fake-project",
},
},
&rolloutsapi.AnalysisTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "test-template",
Namespace: "fake-project",
},
Spec: rolloutsapi.AnalysisTemplateSpec{
Args: []rolloutsapi.Argument{{Name: "token"}},
},
},
},
credsDB: &credentials.FakeDB{
GetFn: func(
context.Context,
string,
credentials.Type,
string,
) (*credentials.Credentials, error) {
return &credentials.Credentials{Password: "s3cr3t"}, nil
},
},
assertions: func(t *testing.T, c client.Client, vi *kargoapi.VerificationInfo, err error) {
require.NoError(t, err)

require.NotNil(t, vi)
assert.Equal(t, kargoapi.VerificationPhasePending, vi.Phase)
require.NotNil(t, vi.AnalysisRun)

ar := &rolloutsapi.AnalysisRun{}
require.NoError(t, c.Get(t.Context(), types.NamespacedName{
Namespace: vi.AnalysisRun.Namespace,
Name: vi.AnalysisRun.Name,
}, ar))

require.Len(t, ar.Spec.Args, 1)
assert.Equal(t, "token", ar.Spec.Args[0].Name)
require.NotNil(t, ar.Spec.Args[0].Value)
assert.Equal(t, "s3cr3t", *ar.Spec.Args[0].Value)
},
},
{
name: "surfaces error when repoCredentials() is unavailable",
stage: &kargoapi.Stage{
ObjectMeta: metav1.ObjectMeta{
Namespace: "fake-project",
Name: "test-stage",
},
Spec: kargoapi.StageSpec{
Verification: &kargoapi.Verification{
AnalysisTemplates: []kargoapi.AnalysisTemplateReference{
{Name: "test-template"},
},
Args: []kargoapi.AnalysisRunArgument{
{
Name: "token",
Value: "${{ repoCredentials(" +
"'https://github.com/example/repo.git', 'git'" +
").Password }}",
},
},
},
},
},
freightCol: kargoapi.FreightCollection{
ID: "test-collection",
Freight: map[string]kargoapi.FreightReference{
"warehouse": {Name: "test-freight"},
},
},
objects: []client.Object{
&kargoapi.Freight{
ObjectMeta: metav1.ObjectMeta{
Name: "test-freight",
Namespace: "fake-project",
},
},
&rolloutsapi.AnalysisTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "test-template",
Namespace: "fake-project",
},
Spec: rolloutsapi.AnalysisTemplateSpec{
Args: []rolloutsapi.Argument{{Name: "token"}},
},
},
},
// credsDB intentionally left nil.
assertions: func(t *testing.T, _ client.Client, vi *kargoapi.VerificationInfo, err error) {
require.NoError(t, err)

require.NotNil(t, vi)
assert.Equal(t, kargoapi.VerificationPhaseError, vi.Phase)
assert.Contains(t, vi.Message, "error building AnalysisRun")
assert.Contains(t, vi.Message, "repoCredentials is not available")
},
},
{
name: "handles analysis run build error",
stage: &kargoapi.Stage{
Expand Down Expand Up @@ -4027,7 +4160,8 @@ func TestRegularStageReconciler_startVerification(t *testing.T) {
Build()

r := &RegularStageReconciler{
client: c,
client: c,
credentialsDB: tt.credsDB,
cfg: ReconcilerConfig{
RolloutsIntegrationEnabled: !tt.rolloutsDisabled,
RolloutsControllerInstanceID: "test-instance",
Expand Down
Loading
Loading