Files
threshold/internal/server/repository_worker.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

835 lines
30 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package server
import (
"context"
stderrors "errors"
"fmt"
"strings"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/server/store"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-dbw"
"github.com/hashicorp/nodeenrollment"
"github.com/hashicorp/nodeenrollment/registration"
"github.com/hashicorp/nodeenrollment/types"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
var ErrCannotUpdateKmsWorkerViaApi = stderrors.New("cannot update a kms worker's basic information via api")
// DeleteWorker will delete a worker from the repository.
func (r *Repository) DeleteWorker(ctx context.Context, publicId string, _ ...Option) (int, error) {
const op = "server.(Repository).DeleteWorker"
if publicId == "" {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
worker := allocWorker()
worker.Worker.PublicId = publicId
var rowsDeleted int
_, err := r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
deleteWorker := worker.clone()
var err error
rowsDeleted, err = w.Delete(
ctx,
deleteWorker,
)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rowsDeleted > 1 {
// return err, which will result in a rollback of the delete
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted")
}
return nil
},
)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId)))
}
if rowsDeleted == 0 {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for worker with workerId: %s", publicId)))
}
return rowsDeleted, nil
}
// LookupWorkerByName returns the worker with the provided name. In the event
// that no worker is found that matches then nil, nil will be returned.
func (r *Repository) LookupWorkerByName(ctx context.Context, name string) (*Worker, error) {
const op = "server.(Repository).LookupWorkerByName"
switch {
case name == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "name is empty")
}
w, err := lookupWorkerByName(ctx, r.reader, name)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return w, nil
}
func lookupWorkerByName(ctx context.Context, reader db.Reader, name string) (*Worker, error) {
const op = "server.lookupWorkerByName"
switch {
case isNil(reader):
return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil")
case name == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "name is empty")
}
wAgg := &workerAggregate{}
err := reader.LookupWhere(ctx, &wAgg, "name = ?", []any{name})
if err != nil {
if errors.IsNotFoundError(err) {
return nil, nil
}
return nil, errors.Wrap(ctx, err, op)
}
w, err := wAgg.toWorker(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return w, nil
}
func (r *Repository) LookupWorkerIdByKeyId(ctx context.Context, keyId string) (string, error) {
const op = "server.(Repository).LookupWorkerIdByKeyId"
switch {
case keyId == "":
return "", errors.New(ctx, errors.InvalidParameter, op, "keyId is empty")
}
// We're searching for a workerAuth record based on worker id
worker := allocWorkerAuth()
worker.WorkerKeyIdentifier = keyId
err := r.reader.LookupById(ctx, worker)
if err != nil {
if errors.IsNotFoundError(err) {
return "", nil
}
return "", errors.Wrap(ctx, err, op)
}
return worker.WorkerId, nil
}
// LookupWorker returns the worker for the provided publicId. This returns
// nil nil in the situation where no worker can be found with that public id.
func (r *Repository) LookupWorker(ctx context.Context, publicId string, _ ...Option) (*Worker, error) {
const op = "server.(Repository).LookupWorker"
switch {
case publicId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "publicId is empty")
}
w, err := lookupWorker(ctx, r.reader, publicId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return w, nil
}
// lookupWorker returns the worker for the provided id. This returns
// nil nil in the situation where no worker can be found with that public id.
func lookupWorker(ctx context.Context, reader db.Reader, id string) (*Worker, error) {
const op = "server.lookupWorker"
switch {
case id == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "id is empty")
}
wAgg := &workerAggregate{}
wAgg.PublicId = id
err := reader.LookupById(ctx, wAgg)
if err != nil {
if errors.IsNotFoundError(err) {
return nil, nil
}
return nil, errors.Wrap(ctx, err, op)
}
w, err := wAgg.toWorker(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return w, nil
}
// ListWorkers will return a listing of Workers and honor the WithLimit option.
// Supported options: WithWorkerType, WithActiveWorkers, WithLiveness,
// WithWorkerPool, WithLimit
//
// If WithLiveness is zero the default liveness value is used, if it is negative
// then the last status update time is ignored.
// If WithLimit < 0, then unlimited results are returned. If WithLimit == 0, then
// default limits are used for results. WithWorkerPool can be provided with a
// non-zero length slice of worker ids to restrict the returned workers to only
// ones with the ids provided.
func (r *Repository) ListWorkers(ctx context.Context, scopeIds []string, opt ...Option) ([]*Worker, error) {
const op = "server.(Repository).ListWorkers"
opts := GetOpts(opt...)
newOpts := []Option{}
// handle the WithLimit default for the repo
switch {
case opts.withLimit != 0:
newOpts = append(newOpts, WithLimit(opts.withLimit))
default:
newOpts = append(newOpts, WithLimit(r.defaultLimit))
}
// handle the WithLiveness default
switch {
case opts.withLiveness != 0:
newOpts = append(newOpts, WithLiveness(opts.withLiveness))
default:
newOpts = append(newOpts, WithLiveness(DefaultLiveness))
}
newOpts = append(newOpts, WithWorkerType(opts.withWorkerType))
newOpts = append(newOpts, WithActiveWorkers(opts.withActiveWorkers))
newOpts = append(newOpts, WithWorkerPool(opts.withWorkerPool))
return ListWorkers(ctx, r.reader, scopeIds, newOpts...)
}
// ListWorkers will return a listing of Workers and honor the WithLimit option.
// Supported options: WithWorkerType, WithActiveWorkers, WithLiveness,
// WithWorkerPool, WithLimit
//
// If WithLiveness is zero the default liveness value is used, if it is negative
// then the last status update time is ignored.
// If WithLimit < 0, then unlimited results are returned. If WithLimit == 0, then
// default limits are used for results. WithWorkerPool can be provided with a
// non-zero length slice of worker ids to restrict the returned workers to only
// ones with the ids provided.
func ListWorkers(ctx context.Context, reader db.Reader, scopeIds []string, opt ...Option) ([]*Worker, error) {
const op = "server.ListWorkers"
switch {
case len(scopeIds) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope ids set")
}
opts := GetOpts(opt...)
liveness := opts.withLiveness
if liveness == 0 {
liveness = DefaultLiveness
}
var where []string
var whereArgs []any
if liveness > 0 {
where = append(where, fmt.Sprintf("last_status_time > now() - interval '%d seconds'", uint32(liveness.Seconds())))
}
if len(scopeIds) > 0 {
where = append(where, "scope_id in (?)")
whereArgs = append(whereArgs, scopeIds)
}
switch opts.withWorkerType {
case "":
case KmsWorkerType, PkiWorkerType:
where = append(where, "type = ?")
whereArgs = append(whereArgs, opts.withWorkerType.String())
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown worker type %v", opts.withWorkerType))
}
if opts.withActiveWorkers {
where = append(where, "operational_state = ?")
whereArgs = append(whereArgs, ActiveOperationalState.String())
}
if len(opts.withWorkerPool) > 0 {
where = append(where, "public_id in (?)")
whereArgs = append(whereArgs, opts.withWorkerPool)
}
limit := db.DefaultLimit
if opts.withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
var wAggs []*workerAggregate
if err := reader.SearchWhere(
ctx,
&wAggs,
strings.Join(where, " and "),
whereArgs,
db.WithLimit(limit),
); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error searching for workers"))
}
workers := make([]*Worker, 0, len(wAggs))
for _, a := range wAggs {
w, err := a.toWorker(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error converting workerAggregate to Worker"))
}
workers = append(workers, w)
}
return workers, nil
}
// UpsertWorkerStatus will update the address and last status time for a worker.
// If the worker is a kms worker that hasn't been seen yet, it'll attempt to
// create a new one, but will return an error if another worker (kms or other)
// has the same name. This returns the Worker object with the changes applied.
// The WithPublicId, WithKeyId, and WithUpdateTags options are
// the only ones used. All others are ignored.
// Workers are intentionally not oplogged.
func (r *Repository) UpsertWorkerStatus(ctx context.Context, worker *Worker, opt ...Option) (*Worker, error) {
const op = "server.UpsertWorkerStatus"
opts := GetOpts(opt...)
switch {
case worker == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker is nil")
case worker.GetAddress() == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker reported address is empty")
case worker.ScopeId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "scope id is empty")
case worker.PublicId != "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker id is not empty")
case worker.GetName() == "" && opts.withKeyId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker keyId and reported name are both empty; one is required")
case worker.OperationalState == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker operational state is empty")
}
var workerId string
var err error
switch {
case opts.withPublicId != "":
workerId = opts.withPublicId
case opts.withKeyId != "":
workerId, err = r.LookupWorkerIdByKeyId(ctx, opts.withKeyId)
if err != nil || workerId == "" {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error finding worker by keyId"))
}
default:
// generating the worker id based off of the scope and name ensures
// that if 2 kms workers are making requests with the same name they
// are treated as the same worker. Allowing this only for kms workers
// also ensures that we maintain the unique name constraint between pki
// workers and kms workers.
workerId, err = NewWorkerIdFromScopeAndName(ctx, worker.GetScopeId(), worker.GetName())
if err != nil || workerId == "" {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating a worker id"))
}
}
var ret *Worker
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
workerClone := worker.clone()
workerClone.PublicId = workerId
switch {
case opts.withKeyId != "":
// This case goes first because a key ID being supplied should
// be a clear indicator that we are working with a PKI (or
// KMS-PKI) workerClone, and a lack of one a clear indication we
// are working with a KMS workerClone.
//
// Note: unlike in the below case, this purposefully leaves out
// "description" since we want description changes for (non
// KMS-PKI) PKI-based workers to come via API only. We can't
// really guard on this in the DB so we need to be sure to not
// include it here.
n, err := w.Update(ctx, workerClone, []string{"address", "ReleaseVersion", "OperationalState"}, nil)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update status of pki worker"))
}
switch n {
case 0:
return errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("failed to find worker with key id %q", opts.withKeyId))
case 1:
break
default:
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("multiple records found when updating worker with id %q", workerClone.GetPublicId()))
}
case workerClone.GetName() != "":
workerClone.Type = KmsWorkerType.String()
workerCreateConflict := &db.OnConflict{
Target: db.Columns{"public_id"},
Action: append(db.SetColumns([]string{"address", "release_version", "operational_state"}),
db.SetColumnValues(map[string]any{"last_status_time": "now()"})...),
}
var withRowsAffected int64
err := w.Create(ctx, workerClone, db.WithOnConflict(workerCreateConflict), db.WithReturnRowsAffected(&withRowsAffected),
// The intent of this WithWhere option is to operate with the OnConflict such that the action
// taken by the OnConflict only applies if the conflict is on a row that is returned by this where
// statement, otherwise it should error out.
db.WithWhere("server_worker.type = 'kms'"))
if err == nil && workerClone.Description != worker.Description {
_, err = w.Update(ctx, workerClone, []string{"description"}, nil)
}
switch {
case err != nil:
return errors.Wrap(ctx, err, op, errors.WithMsg("error creating a worker"))
case withRowsAffected == 0:
return errors.New(ctx, errors.NotUnique, op, "error updating worker")
}
}
// If we've been told to update tags, we need to clean out old
// ones and add new ones. Within the current transaction, simply
// delete all tags for the given worker, then add the new ones
// we've been sent.
if opts.withUpdateTags {
if err := setWorkerTags(ctx, w, workerClone.GetPublicId(), ConfigurationTagSource, workerClone.inputTags); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error setting worker tags"))
}
}
wAgg := &workerAggregate{PublicId: workerClone.GetPublicId()}
if err := reader.LookupById(ctx, wAgg); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error looking up worker aggregate"))
}
ret, err = wAgg.toWorker(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error converting worker aggregate to worker"))
}
return nil
},
)
if err != nil {
return nil, err
}
return ret, nil
}
// setWorkerTags removes all existing tags from the same source and worker id
// and creates new ones based on the ones provided. This function should be
// called from inside a db transaction.
// Workers/worker tags are intentionally not oplogged.
func setWorkerTags(ctx context.Context, w db.Writer, id string, ts TagSource, tags []*Tag) error {
const op = "server.setWorkerTags"
switch {
case !ts.isValid():
return errors.New(ctx, errors.InvalidParameter, op, "invalid tag source provided")
case id == "":
return errors.New(ctx, errors.InvalidParameter, op, "worker id is empty")
case isNil(w):
return errors.New(ctx, errors.InvalidParameter, op, "db.Writer is nil")
}
_, err := w.Exec(ctx, deleteTagsByWorkerIdSql, []any{ts.String(), id})
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("couldn't delete existing tags for worker %q", id)))
}
// If tags were cleared out entirely, then we'll have nothing
// to do here, e.g., it will result in deletion of all tags.
// Otherwise, go through and stage each tuple for insertion
// below.
if len(tags) > 0 {
uTags := make([]any, 0, len(tags))
for _, v := range tags {
if v == nil {
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("found nil tag value for worker %s", id))
}
uTags = append(uTags, &store.WorkerTag{
WorkerId: id,
Key: v.Key,
Value: v.Value,
Source: ts.String(),
})
}
if err = w.CreateItems(ctx, uTags); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error creating tags for worker %q", id)))
}
}
return nil
}
// UpdateWorker will update a worker in the repository and return the resulting
// worker. fieldMaskPaths provides field_mask.proto paths for fields that should
// be updated. Fields will be set to NULL if the field is a zero value and
// included in fieldMask. Name, Description, and Address are the only updatable
// fields, if no updatable fields are included in the fieldMaskPaths, then an
// error is returned. If any paths besides those listed above are included in
// the path then an error is returned. If the worker is a KMS worker (whether
// via the old registration method or pki-kms) name updates will be disallowed.
func (r *Repository) UpdateWorker(ctx context.Context, worker *Worker, version uint32, fieldMaskPaths []string, opt ...Option) (*Worker, int, error) {
const (
nameField = "name"
descField = "description"
)
const op = "server.(Repository).UpdateWorker"
switch {
case worker == nil:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "worker is nil")
case worker.PublicId == "":
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
case version == 0:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "version is zero")
}
for _, f := range fieldMaskPaths {
switch {
case strings.EqualFold(nameField, f):
case strings.EqualFold(descField, f):
default:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, fmt.Sprintf("invalid field mask: %s", f))
}
}
var dbMask, nullFields []string
dbMask, nullFields = dbw.BuildUpdatePaths(
map[string]any{
nameField: worker.Name,
descField: worker.Description,
},
fieldMaskPaths,
nil,
)
if len(dbMask) == 0 && len(nullFields) == 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "no fields to update")
}
var rowsUpdated int
var ret *Worker
var err error
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
// First we need to do a lookup so we can validate that this
// function is not being used for workers registered via KMS-PKI
// means
wAgg := &workerAggregate{PublicId: worker.GetPublicId()}
if err := reader.LookupById(ctx, wAgg); err != nil {
return errors.Wrap(ctx, err, op)
}
// If it's a KMS-PKI worker we do not want to allow
// name/description/other updates via the API, it should only come
// in via the upsert mechanism via status updates. If the public ID
// is predictably generated in the KMS fashion, it's a KMS-PKI
// worker.
workerId, err := NewWorkerIdFromScopeAndName(ctx, wAgg.ScopeId, wAgg.Name)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("error generating worker id in kms-pki name check case"))
}
if workerId == worker.PublicId {
return errors.Wrap(ctx, ErrCannotUpdateKmsWorkerViaApi, op, errors.WithCode(errors.InvalidParameter))
}
worker := worker.clone()
rowsUpdated, err = w.Update(ctx, worker, dbMask, nullFields, db.WithVersion(&version),
db.WithWhere("server_worker.type = 'pki'"))
if err != nil {
return errors.Wrap(ctx, err, op)
}
if worker.Type == KmsWorkerType.String() {
return errors.New(ctx, errors.InvalidParameter, op, "cannot update a KMS worker")
}
if rowsUpdated > 1 {
// return err, which will result in a rollback of the update
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated")
}
wAgg = &workerAggregate{PublicId: worker.GetPublicId()}
if err := reader.LookupById(ctx, wAgg); err != nil {
return errors.Wrap(ctx, err, op)
}
if ret, err = wAgg.toWorker(ctx); err != nil {
return err
}
return nil
},
)
if err != nil {
if errors.IsUniqueError(err) {
return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("worker with name %q already exists", worker.Name))
}
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", worker.GetPublicId())))
}
return ret, rowsUpdated, nil
}
// CreateWorker will create a worker in the repository and return the written
// worker. Creating a worker is not intentionally oplogged. A worker's
// ReportedStatus and Tags are intentionally ignored when creating a worker (not
// included). Currently, a worker can only be created in the global scope
//
// Options supported: WithNewIdFunc (this option is likely only useful for
// tests), WithFetchNodeCredentialsRequest,
// WithCreateControllerLedActivationToken. The latter two are mutually
// exclusive.
func (r *Repository) CreateWorker(ctx context.Context, worker *Worker, opt ...Option) (*Worker, error) {
const op = "server.CreateWorker"
opts := GetOpts(opt...)
switch {
case worker == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing worker")
case worker.PublicId != "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "public id is not empty")
case worker.ScopeId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
case worker.ScopeId != scope.Global.String():
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("scope id must be %q", scope.Global.String()))
case worker.Address != "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "address is not empty")
case worker.LastStatusTime != nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "last status time is not nil")
case opts.WithFetchNodeCredentialsRequest != nil && opts.WithCreateControllerLedActivationToken:
return nil, errors.New(ctx, errors.InvalidParameter, op, "fetch node credentials request and controller led activation token option cannot both be set")
}
worker.OperationalState = UnknownOperationalState.String()
var err error
if worker.PublicId, err = opts.withNewIdFunc(ctx); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate worker id"))
}
state, err := structpb.NewStruct(map[string]any{
"worker_id": worker.PublicId,
})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating state struct", err))
}
var workerAuthRepo *WorkerAuthRepositoryStorage
databaseWrapper, err := r.kms.GetWrapper(context.Background(), worker.ScopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
var returnedWorker *Worker
if _, err := r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(read db.Reader, w db.Writer) error {
returnedWorker = worker.clone()
returnedWorker.Type = PkiWorkerType.String()
if err := w.Create(
ctx,
returnedWorker,
); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker"))
}
switch {
case opts.WithFetchNodeCredentialsRequest != nil:
workerAuthRepo, err = NewRepositoryStorage(ctx, read, w, r.kms)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker auth repository"))
}
nodeInfo, err := registration.AuthorizeNode(ctx, workerAuthRepo, opts.WithFetchNodeCredentialsRequest, nodeenrollment.WithSkipStorage(true))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to authorize node"))
}
nodeInfo.State = state
if err := StoreNodeInformationTx(ctx, read, w, r.kms, worker.ScopeId, nodeInfo); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to store node information"))
}
case opts.WithCreateControllerLedActivationToken:
tokenId, activationToken, err := registration.CreateServerLedActivationToken(ctx, nil, &types.ServerLedRegistrationRequest{},
nodeenrollment.WithSkipStorage(true),
nodeenrollment.WithState(state))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create controller-led activation token"))
}
creationTime := timestamppb.Now()
creationTimeBytes, err := proto.Marshal(creationTime)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal timestamp for activation token"))
}
activationTokenEntry, err := newWorkerAuthServerLedActivationToken(ctx, worker.PublicId, tokenId, creationTimeBytes)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in-memory activation token"))
}
if err := activationTokenEntry.encrypt(ctx, databaseWrapper); err != nil {
return errors.Wrap(ctx, err, op)
}
if err := w.Create(
ctx,
activationTokenEntry,
); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker activation token in storage"))
}
returnedWorker.ControllerGeneratedActivationToken = activationToken
}
return nil
},
); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return returnedWorker, nil
}
// AddWorkerTags adds specified api tags to the repo worker and returns its new tags.
// No options are currently supported.
func (r *Repository) AddWorkerTags(ctx context.Context, workerId string, workerVersion uint32, tags []*Tag, _ ...Option) ([]*Tag, error) {
const op = "server.(Repository).AddWorkerTags"
switch {
case workerId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker public id is empty")
case workerVersion == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
case len(tags) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "no tags provided")
}
worker, err := lookupWorker(ctx, r.reader, workerId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if worker == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("no worker found with public id %s", workerId))
}
newTags := append(worker.apiTags, tags...)
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
worker := worker.clone()
worker.PublicId = workerId
worker.Version = workerVersion + 1
rowsUpdated, err := w.Update(ctx, worker, []string{"Version"}, nil, db.WithVersion(&workerVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update worker version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated worker version and %d rows updated", rowsUpdated))
}
err = setWorkerTags(ctx, w, workerId, ApiTagSource, newTags)
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
})
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return newTags, nil
}
// SetWorkerTags clears the current repo worker's api tags and sets them from the input parameters.
// Returns the current repo worker tags. No options are currently supported.
func (r *Repository) SetWorkerTags(ctx context.Context, workerId string, workerVersion uint32, tags []*Tag, _ ...Option) ([]*Tag, error) {
const op = "server.(Repository).SetWorkerTags"
switch {
case workerId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker public id is empty")
case workerVersion == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
worker, err := lookupWorker(ctx, r.reader, workerId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if worker == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("no worker found with public id %s", workerId))
}
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
worker := worker.clone()
worker.PublicId = workerId
worker.Version = workerVersion + 1
rowsUpdated, err := w.Update(ctx, worker, []string{"Version"}, nil, db.WithVersion(&workerVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update worker version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated worker version and %d rows updated", rowsUpdated))
}
err = setWorkerTags(ctx, w, workerId, ApiTagSource, tags)
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
})
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return tags, nil
}
// DeleteWorkerTags deletes specified api worker tags from the repo. Returns the number of rows deleted.
// No options are currently supported.
func (r *Repository) DeleteWorkerTags(ctx context.Context, workerId string, workerVersion uint32, tags []*Tag, _ ...Option) (int, error) {
const op = "server.(Repository).DeleteWorkerTags"
switch {
case workerId == "":
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "worker public id is empty")
case workerVersion == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
case len(tags) == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no tags provided")
}
worker, err := lookupWorker(ctx, r.reader, workerId)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
if worker == nil {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("no worker found with public id %s", workerId))
}
rowsDeleted := 0
deleteTags := make([]any, 0, len(tags))
for _, t := range tags {
if t == nil {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "found nil tag value in input")
}
deleteTags = append(deleteTags, &store.WorkerTag{
WorkerId: workerId,
Key: t.Key,
Value: t.Value,
Source: ApiTagSource.String(),
})
}
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
worker := worker.clone()
worker.PublicId = workerId
worker.Version = workerVersion + 1
rowsUpdated, err := w.Update(ctx, worker, []string{"Version"}, nil, db.WithVersion(&workerVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update worker version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated worker version and %d rows updated", rowsUpdated))
}
rowsDeleted, err = w.DeleteItems(ctx, deleteTags)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete worker tags"))
}
if rowsDeleted != len(deleteTags) {
return errors.New(ctx, errors.MultipleRecords, op,
fmt.Sprintf("unable to delete specified tag: tags deleted %d did not match request for %d", rowsDeleted, len(tags)))
}
return nil
})
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return rowsDeleted, nil
}