Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

feature: Enable runner targeting by labels specified in runner profile #3145

Merged
merged 11 commits into from Mar 28, 2022
8 changes: 8 additions & 0 deletions .changelog/3145.txt
@@ -0,0 +1,8 @@
```release-note:feature
**Targetable Runners with Labels**: Waypoint now supports runner profiles that target specific on-demand runners by labels.
Projects and/or Apps can be configured to use a specific runner profile, identified by name.
The runner profile will target all operations to a specific on-demand runner by ID, or labels on the runner.
```
```release-note:improvement
cli: `runner profile` command set now supports target runner labels
```
8 changes: 6 additions & 2 deletions internal/cli/runner_profile_inspect.go
@@ -1,8 +1,10 @@
package cli

import (
"encoding/json"
"fmt"

"github.com/posener/complete"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
Expand All @@ -11,7 +13,6 @@ import (
"github.com/hashicorp/waypoint/internal/clierrors"
"github.com/hashicorp/waypoint/internal/pkg/flag"
pb "github.com/hashicorp/waypoint/pkg/server/gen"
"github.com/posener/complete"
)

type RunnerProfileInspectCommand struct {
Expand Down Expand Up @@ -84,6 +85,9 @@ func (c *RunnerProfileInspectCommand) Run(args []string) int {
targetRunner = "*"
case *pb.Ref_Runner_Id:
targetRunner = t.Id.Id
case *pb.Ref_Runner_Labels:
s, _ := json.Marshal(t.Labels.Labels)
targetRunner = "labels: " + string(s)
}
}
c.ui.Output("Runner profile:", terminal.WithHeaderStyle())
Expand All @@ -104,7 +108,7 @@ func (c *RunnerProfileInspectCommand) Run(args []string) int {
Name: "Plugin Type", Value: config.PluginType,
},
{
Name: "Target Runner ID", Value: targetRunner,
Name: "Target Runner", Value: targetRunner,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a big deal, but a target runner can have only one of any, id, or lables, so should this be one field that is like Target: instead that follows the format of list or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm that's a good point actually, right now it's not hard-coded to restrict only one of ID or Labels (so you could define both), but ID always takes precedence over labels. I think I was thinking at one point "if they define both, but we can't find the runner by ID, then we take the labels instead". But since we ended up removing that "runner with this ID" exists check, we should allow only ID or Label, not both.

{
Name: "Environment Variables", Value: config.EnvironmentVariables,
Expand Down
14 changes: 9 additions & 5 deletions internal/cli/runner_profile_list.go
@@ -1,14 +1,15 @@
package cli

import (
"github.com/hashicorp/waypoint/internal/clierrors"
"github.com/hashicorp/waypoint/internal/pkg/flag"
pb "github.com/hashicorp/waypoint/pkg/server/gen"
"encoding/json"
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick, across the project we typically do the pattern below, so we should have a newline between this and the next imports. Lots of tools automatically insert and break this once in awhile, not a big deal usually just manually clean it up.

import (
  stdlib

  external

  internal
)


"github.com/posener/complete"
empty "google.golang.org/protobuf/types/known/emptypb"

"github.com/hashicorp/waypoint-plugin-sdk/terminal"
empty "google.golang.org/protobuf/types/known/emptypb"
"github.com/hashicorp/waypoint/internal/clierrors"
"github.com/hashicorp/waypoint/internal/pkg/flag"
pb "github.com/hashicorp/waypoint/pkg/server/gen"
)

type RunnerProfileListCommand struct {
Expand Down Expand Up @@ -37,7 +38,7 @@ func (c *RunnerProfileListCommand) Run(args []string) int {

c.ui.Output("Runner profiles")

tbl := terminal.NewTable("Name", "Plugin Type", "OCI Url", "Target Runner ID",
tbl := terminal.NewTable("Name", "Plugin Type", "OCI Url", "Target Runner",
"Default")

for _, p := range resp.Configs {
Expand All @@ -53,6 +54,9 @@ func (c *RunnerProfileListCommand) Run(args []string) int {
targetRunner = "*"
case *pb.Ref_Runner_Id:
targetRunner = t.Id.Id
case *pb.Ref_Runner_Labels:
s, _ := json.Marshal(t.Labels.Labels)
targetRunner = "labels: " + string(s)
}
}

Expand Down
49 changes: 33 additions & 16 deletions internal/cli/runner_profile_set.go
Expand Up @@ -5,29 +5,31 @@ import (
"path/filepath"
"strings"

"github.com/posener/complete"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
hcljson "github.com/hashicorp/hcl/v2/json"
"github.com/hashicorp/waypoint-plugin-sdk/terminal"
"github.com/hashicorp/waypoint/internal/clierrors"
"github.com/hashicorp/waypoint/internal/pkg/flag"
pb "github.com/hashicorp/waypoint/pkg/server/gen"
"github.com/posener/complete"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type RunnerProfileSetCommand struct {
*baseCommand
//TODO(XX): after `-env-vars` as a slice is deprecated, rename flagEnvVar to flagEnvVars
flagName string
flagOCIUrl string
flagEnvVar map[string]string
flagEnvVars []string
flagPluginType string
flagPluginConfig string
flagDefault bool
flagTargetRunnerId string
flagName string
flagOCIUrl string
flagEnvVar map[string]string
flagEnvVars []string
flagPluginType string
flagPluginConfig string
flagDefault bool
flagTargetRunnerId string
flagTargetRunnerLabels map[string]string
}

func (c *RunnerProfileSetCommand) Run(args []string) int {
Expand Down Expand Up @@ -107,6 +109,18 @@ func (c *RunnerProfileSetCommand) Run(args []string) int {
},
},
}
if c.flagTargetRunnerLabels != nil {
c.ui.Output("Both -target-runner-id and -target-runner-label detected, only one can be set at a time. ID takes priority.",
terminal.WithWarningStyle())
}
} else if c.flagTargetRunnerLabels != nil {
od.TargetRunner = &pb.Ref_Runner{
Target: &pb.Ref_Runner_Labels{
Labels: &pb.Ref_RunnerLabels{
Labels: c.flagTargetRunnerLabels,
},
},
}
} else {
od.TargetRunner = &pb.Ref_Runner{Target: &pb.Ref_Runner_Any{}}
}
Expand Down Expand Up @@ -285,7 +299,14 @@ func (c *RunnerProfileSetCommand) Flags() *flag.Sets {
Name: "target-runner-id",
Target: &c.flagTargetRunnerId,
Default: "",
Usage: "ID of the remote runner to target for the profile.",
Usage: "ID of the runner to target for this remote runner profile.",
})

f.StringMapVar(&flag.StringMapVar{
Name: "target-runner-label",
Target: &c.flagTargetRunnerLabels,
Usage: "Labels on the runner to target for this remote runner profile. " +
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this could use an additional sentence on what labels for targeting is used for?

"e.g. `-target-runner-label=k=v`. Can be specified multiple times.",
})
})
}
Expand All @@ -305,16 +326,12 @@ func (c *RunnerProfileSetCommand) Synopsis() string {
func (c *RunnerProfileSetCommand) Help() string {
return formatHelp(`
Usage: waypoint runner profile set [OPTIONS]

Create or update a runner profile.

This will register a new runner profile with the given options. If
a runner profile with the same id already exists, this will update the
existing runner profile using the fields that are set.

Waypoint will use a runner profile to spawn containers for
various kinds of work as needed on the platform requested during any given
lifecycle operation.

` + c.Flags().Help())
}
15 changes: 5 additions & 10 deletions internal/server/singleprocess/service_job.go
Expand Up @@ -163,8 +163,8 @@ func (s *service) queueJobReqToJob(
}

// If the job has any target runner, it is a remote job.
// We attempt to spawn an on-demand runner for the job, if it doesn't already have an ODR assigned, use a default.
if _, anyTarget := job.TargetRunner.Target.(*pb.Ref_Runner_Any); anyTarget {
// Use a default ODR profile if it doesn't already have one assigned.
if _, ok := job.TargetRunner.Target.(*pb.Ref_Runner_Any); ok {
if job.OndemandRunner == nil {
ods, err := s.state.OnDemandRunnerConfigDefault()
if err != nil {
Expand All @@ -178,7 +178,7 @@ func (s *service) queueJobReqToJob(
job.OndemandRunner = ods[0]
default:
job.OndemandRunner = ods[rand.Intn(len(ods))]
log.Debug("multiple default on-demand runners detected, chose a random one",
log.Debug("multiple default on-demand runner profiles detected, chose a random one",
"runner-config-id", job.OndemandRunner.Id)
}
}
Expand Down Expand Up @@ -241,7 +241,7 @@ func (s *service) wrapJobWithRunner(
return nil, err
}

// Change our source job to require being run on the launched ODR.
// Change our source job to run on the launched ODR.
source.TargetRunner = &pb.Ref_Runner{
Target: &pb.Ref_Runner_Id{
Id: &pb.Ref_RunnerId{
Expand Down Expand Up @@ -311,7 +311,7 @@ func (s *service) onDemandRunnerStartJob(
}

// We generate a new login token for each ondemand-runner used. This will inherit
// the user of the token to be the user that queue'd the original job, which is
// the user of the token to be the user that queued the original job, which is
// the correct behavior.
token, err := s.newToken(60*time.Minute, DefaultKeyId, nil, &pb.Token{
Kind: &pb.Token_Login_{Login: &pb.Token_Login{
Expand Down Expand Up @@ -380,11 +380,6 @@ func (s *service) onDemandRunnerStartJob(

job.ExpireTime = timestamppb.New(time.Now().Add(dur))

if err != nil {
return nil, "", status.Errorf(codes.FailedPrecondition,
"Failed to get on-demand runner config by name %q, id %q: %s",
job.OndemandRunner.Name, job.OndemandRunner.Id, err)
}
// This will be either "Any" or a specific static runner.
job.TargetRunner = od.TargetRunner

Expand Down