Skip to content
Merged
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
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
# Changelog

## Unreleased — core v0.19.1; Work Contexts are core's, carried opaque
## Unreleased

### Added

- **`moduleauthority.WorkloadTokenFile()`**, the one answer to "where is this
module's projected token": the file `CODEFLY__WORKLOAD_TOKEN_FILE` names
when it is set to a non-blank value, and the pod mount
`/var/run/secrets/codefly/token` otherwise. Since codefly CLI v0.1.187
(codefly-dev/cli#977) a native `codefly run` stands in for the cell and
projects each module workload's token to a file it names in that variable,
because a process cannot read the pod mount path; a module that hardcoded
the mount minted nothing there. The variable and the path are exported as
`WorkloadTokenFileVariable` and `PodWorkloadTokenFile`;
codefly-dev/solution-runtime-go reads the same variable. It resolves the
path only: the token is still read before every mint.

## v0.1.2 — core v0.19.1; Work Contexts are core's, carried opaque

Closes #38. The SDK requires `github.com/codefly-dev/core v0.19.1` and no
longer depends on `github.com/codefly-dev/sdk-go/workcontext` at all: the
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ The SDK surfaces are:
}, moduleauthority.Credentials{
Prefix: modulePrefix,
// Read before every mint: the kubelet rotates the projection.
Token: moduleauthority.ProjectedTokenFile(projectedTokenPath),
Token: moduleauthority.WorkloadTokenFile(),
// Required for a shared module; a tenant-pinned module may omit it.
OrganizationID: organizationID,
})
Expand All @@ -292,8 +292,13 @@ The SDK surfaces are:
`AllowInsecureHTTP` asserts the hop is mesh-protected. SaaS owns the installed
binding's audience, scopes, lifetime, verification, and audit record.

`projectedTokenPath` is the token projection mount path supplied by the
consumer's deployment; there is no default path. The host requires audience
`WorkloadTokenFile()` is where Codefly projected the token for the process
it runs: the file `CODEFLY__WORKLOAD_TOKEN_FILE` names when that is set —
a native `codefly run` (CLI v0.1.187 and later) stands in for the cell and
projects each module's token to a file of its own, since a process cannot
read the pod mount — and the pod mount, `/var/run/secrets/codefly/token`,
otherwise. A consumer with another projection passes
`ProjectedTokenFile(path)` instead, which has no default. The host requires audience
`accounts` and a token lifetime of at most 600 seconds, and verifies the pod,
workload and approved build. The SDK rereads the source for every actual mint
(including a module Work Context refresh), trims whitespace, and rejects an
Expand Down
33 changes: 31 additions & 2 deletions moduleauthority/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,44 @@ type ProjectedTokenSource interface {
// ProjectedTokenFile reads the token from the path its projected volume is
// mounted at, trimmed. An empty file is an error, never an empty token: a
// projection mid-rotation must not be presented as a credential.
// The consumer supplies the mount path; there is no default. The file read
// is limited to 64 KiB, including surrounding whitespace.
// The value is the path to read; ProjectedTokenFile itself has no default.
// The file read is limited to 64 KiB, including surrounding whitespace.
//
// It is the same contract as sdk-go/workcontext's ProjectedTokenFile; a
// consumer that already imports that module may pass that one instead. This
// one exists because moduleauthority does not import sdk-go/workcontext: it
// carries Work Contexts as opaque strings and reads them only through core.
//
// A module running under Codefly passes WorkloadTokenFile(), which names the
// path Codefly projected the token to wherever it runs.
type ProjectedTokenFile string

// WorkloadTokenFileVariable is the environment variable that names the
// module's projected token file to a process that cannot read it at the path a
// pod mounts it at. A native `codefly run` stands in for the cell (codefly-dev/cli
// v0.1.187, #977): it projects each module workload's token to a file of its
// own and names that file here. codefly-dev/solution-runtime-go reads the same
// variable (ProjectedTokenFileEnvironmentVariable).
const WorkloadTokenFileVariable = "CODEFLY__WORKLOAD_TOKEN_FILE"

// PodWorkloadTokenFile is the path a cell's rendered pod mounts the module's
// projected ServiceAccount token at, read-only, for audience `accounts`.
const PodWorkloadTokenFile = "/var/run/secrets/codefly/token"

// WorkloadTokenFile is where this module's projected token is, wherever
// Codefly runs it: the file WorkloadTokenFileVariable names when the variable
// is set to a non-blank value, and PodWorkloadTokenFile otherwise. A process
// under a native run is told; a pod is not, and reads its mount.
//
// It resolves the path, never the token: the file is still read before every
// mint, because the projection rotates under a running process.
func WorkloadTokenFile() ProjectedTokenFile {
if path := strings.TrimSpace(os.Getenv(WorkloadTokenFileVariable)); path != "" {
return ProjectedTokenFile(path)
}
return ProjectedTokenFile(PodWorkloadTokenFile)
}

// ProjectedToken reads and trims the file, bounded.
func (path ProjectedTokenFile) ProjectedToken() (string, error) {
if strings.TrimSpace(string(path)) == "" {
Expand Down
56 changes: 56 additions & 0 deletions moduleauthority/workload_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package moduleauthority

import (
"os"
"path/filepath"
"testing"
)

// A pod is told nothing and reads its mount; a native `codefly run` names the
// file it projected the token to. Without the variable the path is the pod's,
// so a module under a native run that ignored it would open a path the process
// cannot read and mint nothing.
func TestWorkloadTokenFileFollowsTheVariableAndFallsBackToTheMount(t *testing.T) {
t.Run("unset", func(t *testing.T) {
t.Setenv(WorkloadTokenFileVariable, "")
if err := os.Unsetenv(WorkloadTokenFileVariable); err != nil {
t.Fatal(err)
}
if got := WorkloadTokenFile(); got != ProjectedTokenFile(PodWorkloadTokenFile) {
t.Fatalf("WorkloadTokenFile() = %q, want the pod mount %q", got, PodWorkloadTokenFile)
}
})
t.Run("blank", func(t *testing.T) {
t.Setenv(WorkloadTokenFileVariable, " \t")
if got := WorkloadTokenFile(); got != ProjectedTokenFile(PodWorkloadTokenFile) {
t.Fatalf("WorkloadTokenFile() = %q, want the pod mount %q", got, PodWorkloadTokenFile)
}
})
t.Run("named", func(t *testing.T) {
path := filepath.Join(t.TempDir(), "token")
if err := os.WriteFile(path, []byte(" projected-token\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv(WorkloadTokenFileVariable, " "+path+" ")
source := WorkloadTokenFile()
if source != ProjectedTokenFile(path) {
t.Fatalf("WorkloadTokenFile() = %q, want the named file %q", source, path)
}
token, err := source.ProjectedToken()
if err != nil || token != "projected-token" {
t.Fatalf("ProjectedToken() = %q, %v; want the named file's token", token, err)
}
})
}

// The two names are contracts with other repositories, not choices made here:
// the CLI exports the variable to a native run (cli cmd/run/native_cluster.go,
// workloadTokenFileVariable) and a cell's render mounts the token at the path.
func TestWorkloadTokenNamesAreTheCodeflyContract(t *testing.T) {
if WorkloadTokenFileVariable != "CODEFLY__WORKLOAD_TOKEN_FILE" {
t.Errorf("WorkloadTokenFileVariable = %q", WorkloadTokenFileVariable)
}
if PodWorkloadTokenFile != "/var/run/secrets/codefly/token" {
t.Errorf("PodWorkloadTokenFile = %q", PodWorkloadTokenFile)
}
}
Loading