diff --git a/lib/tbot/cli/start_workload_identity_api.go b/lib/tbot/cli/start_workload_identity_api.go new file mode 100644 index 0000000000000..34df4ebc9e48a --- /dev/null +++ b/lib/tbot/cli/start_workload_identity_api.go @@ -0,0 +1,107 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cli + +import ( + "fmt" + "log/slog" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/tbot/config" +) + +// WorkloadIdentityAPICommand implements `tbot start workload-identity-api` and +// `tbot configure workload-identity-api`. +type WorkloadIdentityAPICommand struct { + *sharedStartArgs + *genericMutatorHandler + + // Listen configures where the workload identity API should listen. This + // should be prefixed with a scheme e.g unix:// or tcp://. + Listen string + // WorkloadIdentityName is the name of the workload identity to use. + // --workload-identity-name foo + WorkloadIdentityName string + // WorkloadIdentityLabels is the labels of the workload identity to use. + // --workload-identity-labels x=y,z=a + WorkloadIdentityLabels string +} + +// NewWorkloadIdentityAPICommand initializes the command and flags for the +// `workload-identity-api` service and returns a struct that will contain the +// parse result. +func NewWorkloadIdentityAPICommand(parentCmd *kingpin.CmdClause, action MutatorAction, mode CommandMode) *WorkloadIdentityAPICommand { + // TODO(noah): Unhide this command when feature flag removed + cmd := parentCmd.Command( + "workload-identity-api", + fmt.Sprintf("%s tbot with a workload identity API listener. Compatible with the SPIFFE Workload API and Envoy SDS.", mode), + ).Hidden() + + c := &WorkloadIdentityAPICommand{} + c.sharedStartArgs = newSharedStartArgs(cmd) + c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action) + + cmd.Flag( + "workload-identity-name", + "The name of the workload identity to issue", + ).StringVar(&c.WorkloadIdentityName) + cmd.Flag( + "workload-identity-labels", + "A label-based selector for which workload identities to issue. Multiple labels can be provided using ','.", + ).StringVar(&c.WorkloadIdentityLabels) + cmd.Flag( + "listen-addr", + "The address on which the workload identity API should listen. This should either be prefixed with 'unix://' or 'tcp://'.", + ).Required().StringVar(&c.Listen) + + return c +} + +func (c *WorkloadIdentityAPICommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error { + if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil { + return trace.Wrap(err) + } + + svc := &config.WorkloadIdentityAPIService{ + Listen: c.Listen, + } + + switch { + case c.WorkloadIdentityName != "" && c.WorkloadIdentityLabels != "": + return trace.BadParameter("workload-identity-name and workload-identity-labels flags are mutually exclusive") + case c.WorkloadIdentityName != "": + svc.WorkloadIdentity.Name = c.WorkloadIdentityName + case c.WorkloadIdentityLabels != "": + labels, err := client.ParseLabelSpec(c.WorkloadIdentityLabels) + if err != nil { + return trace.Wrap(err, "parsing --workload-identity-labels") + } + svc.WorkloadIdentity.Labels = map[string][]string{} + for k, v := range labels { + svc.WorkloadIdentity.Labels[k] = []string{v} + } + default: + return trace.BadParameter("workload-identity-name or workload-identity-labels must be specified") + } + + cfg.Services = append(cfg.Services, svc) + + return nil +} diff --git a/lib/tbot/cli/start_workload_identity_api_test.go b/lib/tbot/cli/start_workload_identity_api_test.go new file mode 100644 index 0000000000000..b30a0f2fede4d --- /dev/null +++ b/lib/tbot/cli/start_workload_identity_api_test.go @@ -0,0 +1,75 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cli + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/lib/tbot/config" +) + +func TestNewWorkloadIdentityAPICommand(t *testing.T) { + testStartConfigureCommand(t, NewWorkloadIdentityAPICommand, []startConfigureTestCase{ + { + name: "success", + args: []string{ + "start", + "workload-identity-api", + "--token=foo", + "--join-method=github", + "--proxy-server=example.com:443", + "--listen-addr=tcp://0.0.0.0:8080", + "--workload-identity-labels=*=*,foo=bar", + }, + assertConfig: func(t *testing.T, cfg *config.BotConfig) { + require.Len(t, cfg.Services, 1) + + svc := cfg.Services[0] + wis, ok := svc.(*config.WorkloadIdentityAPIService) + require.True(t, ok) + require.Equal(t, "tcp://0.0.0.0:8080", wis.Listen) + require.Equal(t, map[string][]string{ + "*": {"*"}, + "foo": {"bar"}, + }, wis.WorkloadIdentity.Labels) + }, + }, + { + name: "success name selector", + args: []string{ + "start", + "workload-identity-api", + "--token=foo", + "--join-method=github", + "--proxy-server=example.com:443", + "--listen-addr=unix:///opt/workload.sock", + "--workload-identity-name=jim", + }, + assertConfig: func(t *testing.T, cfg *config.BotConfig) { + require.Len(t, cfg.Services, 1) + + svc := cfg.Services[0] + wis, ok := svc.(*config.WorkloadIdentityAPIService) + require.True(t, ok) + require.Equal(t, "unix:///opt/workload.sock", wis.Listen) + require.Equal(t, "jim", wis.WorkloadIdentity.Name) + }, + }, + }) +}