Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workload Identity: Add workload-identity-api service #50859

Merged
merged 11 commits into from
Jan 17, 2025
107 changes: 107 additions & 0 deletions lib/tbot/cli/start_workload_identity_api.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
// NameSelector is the name of the workload identity to use.
// --name-selector foo
NameSelector string
// LabelSelector is the labels of the workload identity to use.
// --label-selector x=y,z=a
LabelSelector 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(
"name-selector",
"The name of the workload identity to issue",
).StringVar(&c.NameSelector)
GavinFrazar marked this conversation as resolved.
Show resolved Hide resolved
cmd.Flag(
"label-selector",
"A label-based selector for which workload identities to issue. Multiple labels can be provided using ','.",
).StringVar(&c.LabelSelector)
cmd.Flag(
"listen",
"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.NameSelector != "" && c.LabelSelector != "":
return trace.BadParameter("workload-identity-name and workload-identity-labels flags are mutually exclusive")
strideynet marked this conversation as resolved.
Show resolved Hide resolved
case c.NameSelector != "":
svc.Selector.Name = c.NameSelector
case c.LabelSelector != "":
labels, err := client.ParseLabelSpec(c.LabelSelector)
if err != nil {
return trace.Wrap(err, "parsing --workload-identity-labels")
}
svc.Selector.Labels = map[string][]string{}
for k, v := range labels {
svc.Selector.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
}
75 changes: 75 additions & 0 deletions lib/tbot/cli/start_workload_identity_api_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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=tcp://0.0.0.0:8080",
"--label-selector=*=*,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.Selector.Labels)
},
},
{
name: "success name selector",
args: []string{
"start",
"workload-identity-api",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--listen=unix:///opt/workload.sock",
"--name-selector=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.Selector.Name)
},
},
})
}
6 changes: 6 additions & 0 deletions lib/tbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ func (o *ServiceConfigs) UnmarshalYAML(node *yaml.Node) error {
return trace.Wrap(err)
}
out = append(out, v)
case WorkloadIdentityAPIServiceType:
v := &WorkloadIdentityAPIService{}
if err := node.Decode(v); err != nil {
return trace.Wrap(err)
}
out = append(out, v)
default:
return trace.BadParameter("unrecognized service type (%s)", header.Type)
}
Expand Down
6 changes: 6 additions & 0 deletions lib/tbot/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ func TestBotConfig_YAML(t *testing.T) {
Name: "my-workload-identity",
},
},
&WorkloadIdentityAPIService{
Listen: "tcp://127.0.0.1:123",
Selector: WorkloadIdentitySelector{
Name: "my-workload-identity",
},
},
},
},
},
Expand Down
78 changes: 78 additions & 0 deletions lib/tbot/config/service_workload_identity_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 <http://www.gnu.org/licenses/>.

package config

import (
"github.com/gravitational/trace"
"gopkg.in/yaml.v3"

"github.com/gravitational/teleport/lib/tbot/workloadidentity/workloadattest"
)

const WorkloadIdentityAPIServiceType = "workload-identity-api"

var (
_ ServiceConfig = &WorkloadIdentityAPIService{}
)
Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var (
_ ServiceConfig = &WorkloadIdentityAPIService{}
)

From Effective Go:

The appearance of the blank identifier in this construct indicates that the declaration exists only for the type checking, not to create a variable. Don't do this for every type that satisfies an interface, though. By convention, such declarations are only used when there are no static conversions already present in the code, which is a rare event.


// WorkloadIdentityAPIService is the configuration for the
// WorkloadIdentityAPIService
type WorkloadIdentityAPIService struct {
// Listen is the address on which the SPIFFE Workload API server should
// listen. This should either be prefixed with "unix://" or "tcp://".
Listen string `yaml:"listen"`
// Attestors is the configuration for the workload attestation process.
Attestors workloadattest.Config `yaml:"attestors"`
// Selector is the selector for the WorkloadIdentity resource that
// will be used to issue WICs.
Selector WorkloadIdentitySelector `yaml:"selector"`
}

// CheckAndSetDefaults checks the SPIFFESVIDOutput values and sets any defaults.
func (o *WorkloadIdentityAPIService) CheckAndSetDefaults() error {
if o.Listen == "" {
return trace.BadParameter("listen: should not be empty")
}
if err := o.Attestors.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err, "validating attestor")
}
if err := o.Selector.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err, "validating selector")
}
return nil
}

// Type returns the type of the service.
func (o *WorkloadIdentityAPIService) Type() string {
return WorkloadIdentityAPIServiceType
}

// MarshalYAML marshals the WorkloadIdentityOutput into YAML.
func (o *WorkloadIdentityAPIService) MarshalYAML() (interface{}, error) {
type raw WorkloadIdentityAPIService
return withTypeHeader((*raw)(o), WorkloadIdentityAPIServiceType)
}

// UnmarshalYAML unmarshals the WorkloadIdentityOutput from YAML.
func (o *WorkloadIdentityAPIService) UnmarshalYAML(node *yaml.Node) error {
// Alias type to remove UnmarshalYAML to avoid recursion
type raw WorkloadIdentityAPIService
if err := node.Decode((*raw)(o)); err != nil {
return trace.Wrap(err)
}
return nil
}
Loading
Loading