Files
threshold/internal/host/static/repository_host_set_member.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

368 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package static
import (
"context"
"database/sql"
"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/oplog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
)
// AddSetMembers adds hostIds to setId in the repository. It returns a
// slice of all hosts in setId. A host must belong to the same catalog as
// the set to be added. The version must match the current version of the
// setId in the repository.
func (r *Repository) AddSetMembers(ctx context.Context, projectId string, setId string, version uint32, hostIds []string, opt ...Option) ([]*Host, error) {
const op = "static.(Repository).AddSetMembers"
if projectId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no project id")
}
if setId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no set id")
}
if version == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no version")
}
if len(hostIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no host ids")
}
// Create in-memory host set members
members, err := r.newMembers(ctx, setId, hostIds)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
wrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var hosts []*Host
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
set := newHostSetForMembers(setId, version)
metadata := set.oplog(oplog.OpType_OP_TYPE_CREATE)
// Create host set members
msgs, err := createMembers(ctx, w, members)
if err != nil {
return errors.Wrap(ctx, err, op)
}
// Update host set version
if err := updateVersion(ctx, w, wrapper, metadata, msgs, set, version); err != nil {
return errors.Wrap(ctx, err, op)
}
hosts, err = getHosts(ctx, reader, setId, unlimited)
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
})
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return hosts, nil
}
func (r *Repository) newMembers(ctx context.Context, setId string, hostIds []string) ([]any, error) {
var members []any
for _, id := range hostIds {
var m *HostSetMember
m, err := NewHostSetMember(ctx, setId, id)
if err != nil {
return nil, errors.Wrap(ctx, err, "static.newMembers")
}
members = append(members, m)
}
return members, nil
}
func createMembers(ctx context.Context, w db.Writer, members []any) ([]*oplog.Message, error) {
var msgs []*oplog.Message
if err := w.CreateItems(ctx, members, db.NewOplogMsgs(&msgs)); err != nil {
return nil, errors.Wrap(ctx, err, "static.createMembers")
}
return msgs, nil
}
func updateVersion(ctx context.Context, w db.Writer, wrapper wrapping.Wrapper, metadata oplog.Metadata, msgs []*oplog.Message, set *HostSet, version uint32) error {
const op = "static.updateVersion"
setMsg := new(oplog.Message)
rowsUpdated, err := w.Update(ctx, set, []string{"Version"}, nil, db.NewOplogMsg(setMsg), db.WithVersion(&version))
switch {
case err != nil:
return errors.Wrap(ctx, err, op)
case rowsUpdated == 0:
return errors.New(ctx, errors.RecordNotFound, op, "no matching version for host set found")
case rowsUpdated > 1:
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated")
}
msgs = append(msgs, setMsg)
// Write oplog
ticket, err := w.GetTicket(ctx, set)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
if err := w.WriteOplogEntryWith(ctx, wrapper, ticket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
}
const unlimited = -1
func getHosts(ctx context.Context, reader db.Reader, setId string, limit int) ([]*Host, error) {
const whereNoLimit = `public_id in
( select host_id
from static_host_set_member
where set_id = ?
)`
const whereLimit = `public_id in
( select host_id
from static_host_set_member
where set_id = ?
limit ?
)`
params := []any{setId}
var where string
switch limit {
case unlimited:
where = whereNoLimit
default:
where = whereLimit
params = append(params, limit)
}
var hosts []*Host
if err := reader.SearchWhere(ctx, &hosts,
where,
params,
db.WithLimit(limit),
); err != nil {
return nil, errors.Wrap(ctx, err, "static.getHosts")
}
if len(hosts) == 0 {
return nil, nil
}
return hosts, nil
}
// DeleteSetMembers deletes hostIds from setId in the repository. It
// returns the number of hosts deleted from the set. The version must match
// the current version of the setId in the repository.
func (r *Repository) DeleteSetMembers(ctx context.Context, projectId string, setId string, version uint32, hostIds []string, opt ...Option) (int, error) {
const op = "static.(Repository).DeleteSetMembers"
if projectId == "" {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no project id")
}
if setId == "" {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no set id")
}
if version == 0 {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no version")
}
if len(hostIds) == 0 {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no host ids")
}
// Create in-memory host set members
members, err := r.newMembers(ctx, setId, hostIds)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
wrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error {
set := newHostSetForMembers(setId, version)
metadata := set.oplog(oplog.OpType_OP_TYPE_DELETE)
// Delete host set members
msgs, err := deleteMembers(ctx, w, members)
if err != nil {
return errors.Wrap(ctx, err, op)
}
// Update host set version
err = updateVersion(ctx, w, wrapper, metadata, msgs, set, version)
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
})
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return len(hostIds), nil
}
func deleteMembers(ctx context.Context, w db.Writer, members []any) ([]*oplog.Message, error) {
const op = "static.deleteMembers"
var msgs []*oplog.Message
rowsDeleted, err := w.DeleteItems(ctx, members, db.NewOplogMsgs(&msgs))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if rowsDeleted != len(members) {
return nil, errors.E(ctx, errors.WithMsg(fmt.Sprintf("set members deleted %d did not match request for %d", rowsDeleted, len(members))))
}
return msgs, nil
}
// SetSetMembers replaces the hosts in setId with hostIds in the
// repository. It returns a slice of all hosts in setId and a count of
// hosts added or deleted. A host must belong to the same catalog as the
// set to be added. The version must match the current version of the setId
// in the repository. If hostIds is empty, all hosts will be removed setId.
func (r *Repository) SetSetMembers(ctx context.Context, projectId string, setId string, version uint32, hostIds []string, opt ...Option) ([]*Host, int, error) {
const op = "static.(Repository).SetSetMembers"
if projectId == "" {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no project id")
}
if setId == "" {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no set id")
}
if version == 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no version")
}
// TODO(mgaffney) 08/2020: Oplog does not currently support bulk
// operations. Push these operations to the database once bulk
// operations are added.
// NOTE(mgaffney) 08/2020: This establishes a new pattern for
// calculating change sets for "SetMembers" methods. The changes are
// calculated by the database using a single query. Existing
// "SetMembers" methods retrieve all of the members of the set and
// calculate the changes outside of the database. Our default moving
// forward is to use SQL for calculations on the data in the database.
// TODO(mgaffney) 08/2020: Change existing "SetMembers" methods to use
// this pattern.
changes, err := r.changes(ctx, setId, hostIds)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
var deletions, additions []any
for _, c := range changes {
m, err := NewHostSetMember(ctx, setId, c.HostId)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
switch c.Action {
case "delete":
deletions = append(deletions, m)
case "add":
additions = append(additions, m)
}
}
var hosts []*Host
if len(changes) > 0 {
wrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
set := newHostSetForMembers(setId, version)
metadata := set.oplog(oplog.OpType_OP_TYPE_UPDATE)
var msgs []*oplog.Message
// Delete host set members
if len(deletions) > 0 {
deletedMsgs, err := deleteMembers(ctx, w, deletions)
if err != nil {
return errors.Wrap(ctx, err, op)
}
msgs = append(msgs, deletedMsgs...)
metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String())
}
// Add host set members
if len(additions) > 0 {
createdMsgs, err := createMembers(ctx, w, additions)
if err != nil {
return errors.Wrap(ctx, err, op)
}
msgs = append(msgs, createdMsgs...)
metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String())
}
// Update host set version
if err := updateVersion(ctx, w, wrapper, metadata, msgs, set, version); err != nil {
return errors.Wrap(ctx, err, op)
}
hosts, err = getHosts(ctx, reader, setId, unlimited)
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
})
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
}
return hosts, len(changes), nil
}
type change struct {
Action string
HostId string
}
func (r *Repository) changes(ctx context.Context, setId string, hostIds []string) ([]*change, error) {
const op = "static.(Repository).changes"
var inClauseSpots []string
// starts at 2 because there is already a @1 in the query
for i := 2; i < len(hostIds)+2; i++ {
inClauseSpots = append(inClauseSpots, fmt.Sprintf("@%d", i))
}
inClause := strings.Join(inClauseSpots, ",")
if inClause == "" {
inClause = "''"
}
query := fmt.Sprintf(setChangesQuery, inClause)
var params []any
params = append(params, sql.Named("1", setId))
for idx, v := range hostIds {
params = append(params, sql.Named(fmt.Sprintf("%d", idx+2), v))
}
rows, err := r.reader.Query(ctx, query, params)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("query failed"))
}
defer rows.Close()
var changes []*change
for rows.Next() {
var chg change
if err := r.reader.ScanRows(ctx, rows, &chg); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed"))
}
changes = append(changes, &chg)
}
return changes, nil
}