Skip to content
Open
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
23 changes: 7 additions & 16 deletions cmd/ate-setup/internal/steps/substrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"sigs.k8s.io/yaml"

"go.ate.dev/substrate/api/ateapi/v1"
"go.ate.dev/substrate/cmd/ate-setup/internal/log"
"go.ate.dev/substrate/internal/ateclient"
"go.ate.dev/substrate/internal/manifest"
"go.ate.dev/substrate/internal/preview"
"go.ate.dev/substrate/internal/resources"
)
Expand Down Expand Up @@ -56,23 +55,15 @@ func EnsureAtespace(ctx context.Context, client *ateclient.Client, atespace stri
return err
}

// ActorTemplateFromManifest parses a single protojson-shaped YAML or JSON
// document into an ActorTemplate, as `kubectl ate create actor-template`
// does. Parsing is strict: unknown fields are an error, so typos don't
// silently drop configuration.
func ActorTemplateFromManifest(manifest []byte) (*ateapi.ActorTemplate, error) {
jsonData, err := yaml.YAMLToJSON(manifest)
// ActorTemplateFromManifest parses a manifest holding a single
// protojson-shaped ActorTemplate. Empty documents are skipped, such as the
// "---" ko resolve appends.
func ActorTemplateFromManifest(data []byte) (*ateapi.ActorTemplate, error) {
templates, err := manifest.Parse[ateapi.ActorTemplate](data)
if err != nil {
return nil, fmt.Errorf("invalid YAML: %w", err)
}
if string(jsonData) == "null" {
return nil, fmt.Errorf("manifest is empty")
}
template := &ateapi.ActorTemplate{}
if err := protojson.Unmarshal(jsonData, template); err != nil {
return nil, err
}
return template, nil
return manifest.One(templates)
}

// CreateActorTemplate creates the template through the ate API. Actor
Expand Down
12 changes: 12 additions & 0 deletions cmd/ate-setup/internal/steps/substrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,25 @@ func TestActorTemplateFromManifestDemos(t *testing.T) {
}
}

// ko resolve ends every document with a "---" separator.
func TestActorTemplateFromManifestKoOutput(t *testing.T) {
template, err := ActorTemplateFromManifest([]byte("metadata: {atespace: a, name: t}\n---\n"))
if err != nil {
t.Fatalf("ActorTemplateFromManifest: %v", err)
}
if got := template.GetMetadata().GetName(); got != "t" {
t.Errorf("name = %q, want t", got)
}
}

func TestActorTemplateFromManifestErrors(t *testing.T) {
tests := []struct {
name string
manifest string
}{
{name: "empty", manifest: ""},
{name: "not yaml", manifest: ":\t:"},
{name: "second document", manifest: "metadata: {atespace: a, name: t}\n---\nmetadata: {atespace: a, name: u}\n"},
// Strict parsing: a typo must fail rather than silently drop the field.
{name: "unknown field", manifest: "metadata:\n atespace: a\n name: t\nsnapshotsConfig: {}\n"},
}
Expand Down
20 changes: 8 additions & 12 deletions cmd/kubectl-ate/internal/cmd/access_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"

"go.ate.dev/substrate/api/ateapi/v1"
"go.ate.dev/substrate/cmd/kubectl-ate/internal/printer"
"go.ate.dev/substrate/internal/manifest"
"go.ate.dev/substrate/internal/resources"
)

Expand All @@ -40,8 +40,8 @@ var accessPolicyFlags struct {
}

const accessPolicyManifestHelp = `The manifest is a YAML or JSON AccessPolicy, as printed by
"kubectl ate get access-policy -o yaml". Its metadata may be omitted. Members
are principals with a "user:" prefix, e.g.
"kubectl ate get access-policy -o yaml". Its metadata may be omitted. More than
one record is an error. Members are principals with a "user:" prefix, e.g.
"user:system:serviceaccount:team-a:alice".

bindings:
Expand Down Expand Up @@ -118,19 +118,15 @@ func scopeFromFlags(atespace string, global bool) (accessPolicyScope, error) {
return accessPolicyScope{atespace: atespace}, nil
}

// accessPolicyFromManifest parses a single protojson-shaped YAML or JSON
// document into an AccessPolicy. Parsing is strict: unknown fields are an
// error.
// accessPolicyFromManifest parses a manifest holding a single protojson-shaped
// AccessPolicy. An access policy is a singleton in its scope, so more than one
// record is an error.
func accessPolicyFromManifest(data []byte) (*ateapi.AccessPolicy, error) {
jsonData, err := manifestToJSON(data)
policies, err := manifest.Parse[ateapi.AccessPolicy](data)
if err != nil {
return nil, err
}
policy := &ateapi.AccessPolicy{}
if err := protojson.Unmarshal(jsonData, policy); err != nil {
return nil, fmt.Errorf("invalid AccessPolicy: %w", err)
}
return policy, nil
return manifest.One(policies)
}

// loadAccessPolicyManifest reads a manifest from filename, or from in when
Expand Down
89 changes: 56 additions & 33 deletions cmd/kubectl-ate/internal/cmd/actortemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@
package cmd

import (
"context"
"errors"
"fmt"
"io"
"os"

"github.com/spf13/cobra"
"go.ate.dev/substrate/api/ateapi/v1"
"go.ate.dev/substrate/cmd/kubectl-ate/internal/printer"
"google.golang.org/protobuf/encoding/protojson"
"sigs.k8s.io/yaml"
"go.ate.dev/substrate/internal/manifest"
"google.golang.org/grpc"
)

var (
Expand Down Expand Up @@ -92,23 +94,28 @@ var getActorTemplatesCmd = &cobra.Command{

var createActorTemplateCmd = &cobra.Command{
Use: "actor-template -f <manifest>",
Short: "Create an actor template from a manifest",
Long: `Create an actor template from a manifest file.

The manifest is a single YAML (or JSON) document holding one ateapi.ActorTemplate
message in its protojson form, exactly as printed by
"kubectl ate get actor-template <name> -a <atespace> -o yaml".
The template's atespace and name come from the manifest's metadata.
Short: "Create actor templates from a manifest",
Long: `Create actor templates from a manifest file.

A manifest is a valid JSON or YAML file (YAML files may contain multiple
documents, delimited by the YAML document separator: "---"). Each record is a
complete ateapi.ActorTemplate message in its protojson form, exactly as
printed by "kubectl ate get actor-template <name> -a <atespace> -o yaml".
Each template's atespace and name come from its own metadata.
The atespace must already exist.

The templates are created independently: a failure does not stop the rest, and
the failures are reported together at the end. A manifest with one record
prints the template, one with several prints a list.

Actor templates are immutable: there is no update; delete and recreate to change one.`,
Args: noArgs("the template name comes from the manifest passed to -f/--filename", "filename"),
RunE: func(cmd *cobra.Command, args []string) error {
data, err := readFileOrStdin(cmd.InOrStdin(), createActorTemplateFilenameFlag)
if err != nil {
return err
}
template, err := actorTemplateFromManifest(data)
templates, err := manifest.Parse[ateapi.ActorTemplate](data)
if err != nil {
return fmt.Errorf("failed to parse actor template manifest %q: %w", createActorTemplateFilenameFlag, err)
}
Expand All @@ -120,12 +127,46 @@ Actor templates are immutable: there is no update; delete and recreate to change
}
defer apiClient.Close()

resp, err := apiClient.CreateActorTemplate(ctx, &ateapi.CreateActorTemplateRequest{ActorTemplate: template})
created, createErr := createActorTemplates(ctx, apiClient, templates)
err = printCreatedActorTemplates(cmd.OutOrStdout(), len(templates), created, outputFmt)
return errors.Join(createErr, err)
},
}

// printCreatedActorTemplates prints what was created. The shape follows the
// number of templates requested, not created, so a caller can parse the output
// even when some creates failed.
func printCreatedActorTemplates(out io.Writer, requested int, created []*ateapi.ActorTemplate, format string) error {
if len(created) == 0 {
return nil
}
if requested == 1 {
return printer.PrintActorTemplateTo(out, created[0], format)
}
return printer.PrintActorTemplatesTo(out, created, format)
}

// actorTemplateCreator abstracts the CreateActorTemplate RPC.
type actorTemplateCreator interface {
CreateActorTemplate(ctx context.Context, req *ateapi.CreateActorTemplateRequest, opts ...grpc.CallOption) (*ateapi.ActorTemplate, error)
}

// createActorTemplates creates each template independently. A failure does not
// stop the rest. It returns the templates that were created and the joined
// failures.
func createActorTemplates(ctx context.Context, creator actorTemplateCreator, templates []*ateapi.ActorTemplate) ([]*ateapi.ActorTemplate, error) {
var created []*ateapi.ActorTemplate
var errs []error
for _, template := range templates {
resp, err := creator.CreateActorTemplate(ctx, &ateapi.CreateActorTemplateRequest{ActorTemplate: template})
if err != nil {
return fmt.Errorf("failed to create actor template: %w", err)
errs = append(errs, fmt.Errorf("failed to create actor template %q in atespace %q: %w",
template.GetMetadata().GetName(), template.GetMetadata().GetAtespace(), err))
continue
}
return printer.PrintActorTemplateTo(cmd.OutOrStdout(), resp, outputFmt)
},
created = append(created, resp)
}
return created, errors.Join(errs...)
}

var deleteActorTemplateCmd = &cobra.Command{
Expand Down Expand Up @@ -155,24 +196,6 @@ The server also deletes the template's golden actor and golden snapshot.`,
},
}

// actorTemplateFromManifest parses a single protojson-shaped YAML or JSON
// document into an ActorTemplate. Parsing is strict: unknown fields are an
// error, so typos don't silently drop configuration.
func actorTemplateFromManifest(data []byte) (*ateapi.ActorTemplate, error) {
jsonData, err := yaml.YAMLToJSON(data)
if err != nil {
return nil, fmt.Errorf("invalid YAML: %w", err)
}
if string(jsonData) == "null" {
return nil, fmt.Errorf("manifest is empty")
}
template := &ateapi.ActorTemplate{}
if err := protojson.Unmarshal(jsonData, template); err != nil {
return nil, err
}
return template, nil
}

// readFileOrStdin reads the manifest from path, or from in when path is "-".
func readFileOrStdin(in io.Reader, path string) ([]byte, error) {
if path == "-" {
Expand All @@ -186,7 +209,7 @@ func init() {
getActorTemplatesCmd.Flags().BoolVarP(&getActorTemplateAllAtespacesFlag, "all-atespaces", "A", false, "get actor-templates across all atespaces (mutually exclusive with --atespace)")
getCmd.AddCommand(getActorTemplatesCmd)

createActorTemplateCmd.Flags().StringVarP(&createActorTemplateFilenameFlag, "filename", "f", "", "manifest file holding a single JSON or YAML actor-template document, or - for stdin")
createActorTemplateCmd.Flags().StringVarP(&createActorTemplateFilenameFlag, "filename", "f", "", "JSON or YAML manifest of protojson-shaped ActorTemplates (YAML may hold several documents), or - for stdin")
createCmd.AddCommand(createActorTemplateCmd)

deleteActorTemplateCmd.Flags().StringVarP(&deleteActorTemplateAtespaceFlag, "atespace", "a", "", "the actor-template's atespace")
Expand Down
Loading
Loading