server: break up Intention.Apply monolithic method (#9007)

The Intention.Apply RPC is quite large, so this PR attempts to break it down into smaller functions and dissolves the pre-config-entry approach to the breakdown as it only confused things.
This commit is contained in:
R.B. Boyer 2020-11-13 09:15:39 -06:00 committed by GitHub
parent 491a1ab877
commit c003871c54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 408 additions and 356 deletions

3
.changelog/9007.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
server: break up Intention.Apply monolithic method
```

View File

@ -50,178 +50,6 @@ func (s *Intention) checkIntentionID(id string) (bool, error) {
return true, nil return true, nil
} }
// prepareApplyCreate validates that the requester has permissions to create
// the new intention, generates a new uuid for the intention and generally
// validates that the request is well-formed
//
// Returns an existing service-intentions config entry for this destination if
// one exists.
func (s *Intention) prepareApplyCreate(
ident structs.ACLIdentity,
authz acl.Authorizer,
entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest,
) (*structs.ServiceIntentionsConfigEntry, error) {
if !args.Intention.CanWrite(authz) {
var accessorID string
if ident != nil {
accessorID = ident.ID()
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Intention creation denied due to ACLs", "intention", args.Intention.ID, "accessorID", accessorID)
return nil, acl.ErrPermissionDenied
}
// If no ID is provided, generate a new ID. This must be done prior to
// appending to the Raft log, because the ID is not deterministic. Once
// the entry is in the log, the state update MUST be deterministic or
// the followers will not converge.
if args.Intention.ID != "" {
return nil, fmt.Errorf("ID must be empty when creating a new intention")
}
var err error
args.Intention.ID, err = lib.GenerateUUID(s.checkIntentionID)
if err != nil {
return nil, err
}
// Set the created at
args.Intention.CreatedAt = time.Now().UTC()
args.Intention.UpdatedAt = args.Intention.CreatedAt
// Default source type
if args.Intention.SourceType == "" {
args.Intention.SourceType = structs.IntentionSourceConsul
}
args.Intention.DefaultNamespaces(entMeta)
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
return nil, err
}
//nolint:staticcheck
if err := args.Intention.Validate(); err != nil {
return nil, err
}
_, configEntry, err := s.srv.fsm.State().ConfigEntry(nil, structs.ServiceIntentions, args.Intention.DestinationName, args.Intention.DestinationEnterpriseMeta())
if err != nil {
return nil, fmt.Errorf("service-intentions config entry lookup failed: %v", err)
} else if configEntry == nil {
return nil, nil
}
return configEntry.(*structs.ServiceIntentionsConfigEntry), nil
}
// prepareApplyUpdateLegacy validates that the requester has permissions on both the updated and existing
// intention as well as generally validating that the request is well-formed
//
// Returns an existing service-intentions config entry for this destination if
// one exists.
func (s *Intention) prepareApplyUpdateLegacy(
ident structs.ACLIdentity,
authz acl.Authorizer,
entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest,
) (*structs.ServiceIntentionsConfigEntry, error) {
if !args.Intention.CanWrite(authz) {
var accessorID string
if ident != nil {
accessorID = ident.ID()
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Update operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", accessorID)
return nil, acl.ErrPermissionDenied
}
_, configEntry, ixn, err := s.srv.fsm.State().IntentionGet(nil, args.Intention.ID)
if err != nil {
return nil, fmt.Errorf("Intention lookup failed: %v", err)
}
if ixn == nil || configEntry == nil {
return nil, fmt.Errorf("Cannot modify non-existent intention: '%s'", args.Intention.ID)
}
// Perform the ACL check that we have write to the old intention too,
// which must be true to perform any rename. This is the only ACL enforcement
// done for deletions and a secondary enforcement for updates.
if !ixn.CanWrite(authz) {
var accessorID string
if ident != nil {
accessorID = ident.ID()
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Update operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", accessorID)
return nil, acl.ErrPermissionDenied
}
// Prior to v1.9.0 renames of the destination side of an intention were
// allowed, but that behavior doesn't work anymore.
if ixn.DestinationServiceName() != args.Intention.DestinationServiceName() {
return nil, fmt.Errorf("Cannot modify DestinationNS or DestinationName for an intention once it exists.")
}
// We always update the updatedat field.
args.Intention.UpdatedAt = time.Now().UTC()
// Default source type
if args.Intention.SourceType == "" {
args.Intention.SourceType = structs.IntentionSourceConsul
}
args.Intention.DefaultNamespaces(entMeta)
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
return nil, err
}
// Validate. We do not validate on delete since it is valid to only
// send an ID in that case.
//nolint:staticcheck
if err := args.Intention.Validate(); err != nil {
return nil, err
}
return configEntry, nil
}
// prepareApplyDeleteLegacy ensures that the intention specified by the ID in the request exists
// and that the requester is authorized to delete it
//
// Returns an existing service-intentions config entry for this destination if
// one exists.
func (s *Intention) prepareApplyDeleteLegacy(
ident structs.ACLIdentity,
authz acl.Authorizer,
args *structs.IntentionRequest,
) (*structs.ServiceIntentionsConfigEntry, error) {
// If this is not a create, then we have to verify the ID.
_, configEntry, ixn, err := s.srv.fsm.State().IntentionGet(nil, args.Intention.ID)
if err != nil {
return nil, fmt.Errorf("Intention lookup failed: %v", err)
}
if ixn == nil || configEntry == nil {
return nil, fmt.Errorf("Cannot delete non-existent intention: '%s'", args.Intention.ID)
}
// Perform the ACL check that we have write to the old intention. This is
// the only ACL enforcement done for deletions and a secondary enforcement
// for updates.
if !ixn.CanWrite(authz) {
var accessorID string
if ident != nil {
accessorID = ident.ID()
}
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Deletion operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", accessorID)
return nil, acl.ErrPermissionDenied
}
return configEntry, nil
}
var ErrIntentionsNotUpgradedYet = errors.New("Intentions are read only while being upgraded to config entries") var ErrIntentionsNotUpgradedYet = errors.New("Intentions are read only while being upgraded to config entries")
// legacyUpgradeCheck fast fails a write request using the legacy intention // legacyUpgradeCheck fast fails a write request using the legacy intention
@ -271,213 +99,80 @@ func (s *Intention) Apply(
return err return err
} }
var accessorID string
if ident != nil {
accessorID = ident.ID()
}
var ( var (
prevEntry *structs.ServiceIntentionsConfigEntry configOp structs.ConfigEntryOp
upsertEntry *structs.ServiceIntentionsConfigEntry configEntry *structs.ServiceIntentionsConfigEntry
legacyWrite bool legacyWrite bool
noop bool
) )
switch args.Op { switch args.Op {
case structs.IntentionOpCreate: case structs.IntentionOpCreate:
legacyWrite = true legacyWrite = true
configOp, configEntry, err = s.computeApplyChangesLegacyCreate(accessorID, authz, &entMeta, args)
// This variant is just for legacy UUID-based intentions.
prevEntry, err = s.prepareApplyCreate(ident, authz, &entMeta, args)
if err != nil {
return err
}
if prevEntry == nil {
upsertEntry = args.Intention.ToConfigEntry(true)
} else {
upsertEntry = prevEntry.Clone()
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention(true))
}
case structs.IntentionOpUpdate: case structs.IntentionOpUpdate:
// This variant is just for legacy UUID-based intentions.
legacyWrite = true legacyWrite = true
configOp, configEntry, err = s.computeApplyChangesLegacyUpdate(accessorID, authz, &entMeta, args)
prevEntry, err = s.prepareApplyUpdateLegacy(ident, authz, &entMeta, args)
if err != nil {
return err
}
upsertEntry = prevEntry.Clone()
for i, src := range upsertEntry.Sources {
if src.LegacyID == args.Intention.ID {
upsertEntry.Sources[i] = args.Intention.ToSourceIntention(true)
break
}
}
case structs.IntentionOpUpsert: case structs.IntentionOpUpsert:
// This variant is just for config-entry based intentions.
legacyWrite = false legacyWrite = false
configOp, configEntry, err = s.computeApplyChangesUpsert(&entMeta, args)
if args.Intention.ID != "" {
// This is a new-style only endpoint
return fmt.Errorf("ID must not be specified")
}
args.Intention.DefaultNamespaces(&entMeta)
prevEntry, err = s.getServiceIntentionsConfigEntry(args.Intention.DestinationName, args.Intention.DestinationEnterpriseMeta())
if err != nil {
return err
}
sn := args.Intention.SourceServiceName()
// TODO(intentions): have service-intentions validation functions
// return structured errors so that we can rewrite the field prefix
// here so that the validation errors are not misleading.
if prevEntry == nil {
// Meta is NOT permitted here, as it would need to be persisted on
// the enclosing config entry.
if len(args.Intention.Meta) > 0 {
return fmt.Errorf("Meta must not be specified")
}
upsertEntry = args.Intention.ToConfigEntry(false)
} else {
upsertEntry = prevEntry.Clone()
if len(args.Intention.Meta) > 0 {
// Meta is NOT permitted here, but there is one exception. If
// you are updating a previous record, but that record lives
// within a config entry that itself has Meta, then you may
// incidentally ship the Meta right back to consul.
//
// In that case if Meta is provided, it has to be a perfect
// match for what is already on the enclosing config entry so
// it's safe to discard.
if !equalStringMaps(upsertEntry.Meta, args.Intention.Meta) {
return fmt.Errorf("Meta must not be specified, or should be unchanged during an update.")
}
// Now it is safe to discard
args.Intention.Meta = nil
}
found := false
for i, src := range upsertEntry.Sources {
if src.SourceServiceName() == sn {
upsertEntry.Sources[i] = args.Intention.ToSourceIntention(false)
found = true
break
}
}
if !found {
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention(false))
}
}
case structs.IntentionOpDelete: case structs.IntentionOpDelete:
// There are two ways to get this request:
//
// 1) legacy: the ID field is populated
// 2) config-entry: the ID field is NOT populated
if args.Intention.ID == "" { if args.Intention.ID == "" {
// config-entry style: no LegacyID
legacyWrite = false legacyWrite = false
configOp, configEntry, err = s.computeApplyChangesDelete(&entMeta, args)
args.Intention.DefaultNamespaces(&entMeta)
prevEntry, err = s.getServiceIntentionsConfigEntry(args.Intention.DestinationName, args.Intention.DestinationEnterpriseMeta())
if err != nil {
return err
}
// NOTE: validation errors may be misleading!
noop = true
if prevEntry != nil {
sn := args.Intention.SourceServiceName()
upsertEntry = prevEntry.Clone()
for i, src := range upsertEntry.Sources {
if src.SourceServiceName() == sn {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
upsertEntry.Sources = append(upsertEntry.Sources[:i], upsertEntry.Sources[i+1:]...)
if len(upsertEntry.Sources) == 0 {
upsertEntry.Sources = nil
}
noop = false
break
}
}
}
} else { } else {
// legacy style: LegacyID required
legacyWrite = true legacyWrite = true
configOp, configEntry, err = s.computeApplyChangesLegacyDelete(accessorID, authz, &entMeta, args)
prevEntry, err = s.prepareApplyDeleteLegacy(ident, authz, args)
if err != nil {
return err
} }
upsertEntry = prevEntry.Clone()
for i, src := range upsertEntry.Sources {
if src.LegacyID == args.Intention.ID {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
upsertEntry.Sources = append(upsertEntry.Sources[:i], upsertEntry.Sources[i+1:]...)
if len(upsertEntry.Sources) == 0 {
upsertEntry.Sources = nil
}
break
}
}
}
case structs.IntentionOpDeleteAll: case structs.IntentionOpDeleteAll:
// This is an internal operation initiated by the leader and is not // This is an internal operation initiated by the leader and is not
// exposed for general RPC use. // exposed for general RPC use.
fallthrough return fmt.Errorf("Invalid Intention operation: %v", args.Op)
default: default:
return fmt.Errorf("Invalid Intention operation: %v", args.Op) return fmt.Errorf("Invalid Intention operation: %v", args.Op)
} }
if !noop && prevEntry != nil && legacyWrite && !prevEntry.LegacyIDFieldsAreAllSet() { if err != nil {
sn := prevEntry.DestinationServiceName() return err
return fmt.Errorf("cannot use legacy intention API to edit intentions with a destination of %q after editing them via a service-intentions config entry", sn.String())
} }
// setup the reply which will have been filled in by one of the preparedApply* funcs
if legacyWrite { if legacyWrite {
*reply = args.Intention.ID *reply = args.Intention.ID
} else { } else {
*reply = "" *reply = ""
} }
if noop { if configOp == "" {
return nil return nil // no-op
} }
// Commit indirectly by invoking the other RPC handler directly. // Commit indirectly by invoking the other RPC handler directly.
if configOp == structs.ConfigEntryDelete {
configReq := &structs.ConfigEntryRequest{ configReq := &structs.ConfigEntryRequest{
Datacenter: args.Datacenter, Datacenter: args.Datacenter,
WriteRequest: args.WriteRequest, WriteRequest: args.WriteRequest,
} Op: structs.ConfigEntryDelete,
if upsertEntry == nil || len(upsertEntry.Sources) == 0 { Entry: configEntry,
configReq.Op = structs.ConfigEntryDelete
configReq.Entry = &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: prevEntry.Name,
EnterpriseMeta: prevEntry.EnterpriseMeta,
} }
var ignored struct{} var ignored struct{}
return s.configEntryEndpoint.Delete(configReq, &ignored) return s.configEntryEndpoint.Delete(configReq, &ignored)
} else { }
// Update config entry CAS
configReq.Op = structs.ConfigEntryUpsertCAS if configOp != structs.ConfigEntryUpsertCAS {
configReq.Entry = upsertEntry return fmt.Errorf("Invalid Intention config entry operation: %v", configOp)
}
configReq := &structs.ConfigEntryRequest{
Datacenter: args.Datacenter,
WriteRequest: args.WriteRequest,
Op: structs.ConfigEntryUpsertCAS,
Entry: configEntry,
}
var normalizeAndValidateFn func(raw structs.ConfigEntry) error var normalizeAndValidateFn func(raw structs.ConfigEntry) error
if legacyWrite { if legacyWrite {
@ -492,15 +187,316 @@ func (s *Intention) Apply(
} }
var applied bool var applied bool
err := s.configEntryEndpoint.applyInternal(configReq, &applied, normalizeAndValidateFn) if err = s.configEntryEndpoint.applyInternal(configReq, &applied, normalizeAndValidateFn); err != nil {
if err != nil {
return err return err
} }
if !applied { if !applied {
return fmt.Errorf("config entry failed to persist due to CAS failure: kind=%q, name=%q", upsertEntry.Kind, upsertEntry.Name) return fmt.Errorf("config entry failed to persist due to CAS failure: kind=%q, name=%q", configEntry.Kind, configEntry.Name)
} }
return nil return nil
} }
func (s *Intention) computeApplyChangesLegacyCreate(
accessorID string,
authz acl.Authorizer,
entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest,
) (structs.ConfigEntryOp, *structs.ServiceIntentionsConfigEntry, error) {
// This variant is just for legacy UUID-based intentions.
args.Intention.DefaultNamespaces(entMeta)
// Even though the eventual config entry RPC will do an authz check and
// validation, if we do them here too we can generate error messages that
// make more sense for legacy edits.
if !args.Intention.CanWrite(authz) {
sn := args.Intention.SourceServiceName()
dn := args.Intention.DestinationServiceName()
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Intention creation denied due to ACLs",
"source", sn.String(),
"destination", dn.String(),
"accessorID", accessorID)
return "", nil, acl.ErrPermissionDenied
}
// If no ID is provided, generate a new ID. This must be done prior to
// appending to the Raft log, because the ID is not deterministic. Once
// the entry is in the log, the state update MUST be deterministic or
// the followers will not converge.
if args.Intention.ID != "" {
return "", nil, fmt.Errorf("ID must be empty when creating a new intention")
}
var err error
args.Intention.ID, err = lib.GenerateUUID(s.checkIntentionID)
if err != nil {
return "", nil, err
}
// Set the created at
args.Intention.CreatedAt = time.Now().UTC()
args.Intention.UpdatedAt = args.Intention.CreatedAt
// Default source type
if args.Intention.SourceType == "" {
args.Intention.SourceType = structs.IntentionSourceConsul
}
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
return "", nil, err
}
//nolint:staticcheck
if err := args.Intention.Validate(); err != nil {
return "", nil, err
}
_, configEntry, err := s.srv.fsm.State().ConfigEntry(nil, structs.ServiceIntentions, args.Intention.DestinationName, args.Intention.DestinationEnterpriseMeta())
if err != nil {
return "", nil, fmt.Errorf("service-intentions config entry lookup failed: %v", err)
}
if configEntry == nil {
return structs.ConfigEntryUpsertCAS, args.Intention.ToConfigEntry(true), nil
}
prevEntry := configEntry.(*structs.ServiceIntentionsConfigEntry)
if err := checkLegacyIntentionApplyAllowed(prevEntry); err != nil {
return "", nil, err
}
upsertEntry := prevEntry.Clone()
upsertEntry.Sources = append(upsertEntry.Sources, args.Intention.ToSourceIntention(true))
// NOTE: if the append of this source causes a duplicate source name the
// config entry validation will fail so we don't have to check that
// explicitly here.
return structs.ConfigEntryUpsertCAS, upsertEntry, nil
}
func (s *Intention) computeApplyChangesLegacyUpdate(
accessorID string,
authz acl.Authorizer,
entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest,
) (structs.ConfigEntryOp, *structs.ServiceIntentionsConfigEntry, error) {
// This variant is just for legacy UUID-based intentions.
_, prevEntry, ixn, err := s.srv.fsm.State().IntentionGet(nil, args.Intention.ID)
if err != nil {
return "", nil, fmt.Errorf("Intention lookup failed: %v", err)
}
if ixn == nil || prevEntry == nil {
return "", nil, fmt.Errorf("Cannot modify non-existent intention: '%s'", args.Intention.ID)
}
if err := checkLegacyIntentionApplyAllowed(prevEntry); err != nil {
return "", nil, err
}
// Even though the eventual config entry RPC will do an authz check and
// validation, if we do them here too we can generate error messages that
// make more sense for legacy edits.
if !ixn.CanWrite(authz) {
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Update operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", accessorID)
return "", nil, acl.ErrPermissionDenied
}
args.Intention.DefaultNamespaces(entMeta)
// Prior to v1.9.0 renames of the destination side of an intention were
// allowed, but that behavior doesn't work anymore.
if ixn.DestinationServiceName() != args.Intention.DestinationServiceName() {
return "", nil, fmt.Errorf("Cannot modify DestinationNS or DestinationName for an intention once it exists.")
}
// We always update the updatedat field.
args.Intention.UpdatedAt = time.Now().UTC()
// Default source type
if args.Intention.SourceType == "" {
args.Intention.SourceType = structs.IntentionSourceConsul
}
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
return "", nil, err
}
// Validate. We do not validate on delete since it is valid to only
// send an ID in that case.
//nolint:staticcheck
if err := args.Intention.Validate(); err != nil {
return "", nil, err
}
upsertEntry := prevEntry.Clone()
foundMatch := upsertEntry.UpdateSourceByLegacyID(
args.Intention.ID,
args.Intention.ToSourceIntention(true),
)
if !foundMatch {
return "", nil, fmt.Errorf("Cannot modify non-existent intention: '%s'", args.Intention.ID)
}
return structs.ConfigEntryUpsertCAS, upsertEntry, nil
}
func (s *Intention) computeApplyChangesUpsert(
entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest,
) (structs.ConfigEntryOp, *structs.ServiceIntentionsConfigEntry, error) {
// This variant is just for config-entry based intentions.
if args.Intention.ID != "" {
// This is a new-style only endpoint
return "", nil, fmt.Errorf("ID must not be specified")
}
args.Intention.DefaultNamespaces(entMeta)
prevEntry, err := s.getServiceIntentionsConfigEntry(args.Intention.DestinationName, args.Intention.DestinationEnterpriseMeta())
if err != nil {
return "", nil, err
}
// TODO(intentions): have service-intentions validation functions
// return structured errors so that we can rewrite the field prefix
// here so that the validation errors are not misleading.
if prevEntry == nil {
// Meta is NOT permitted here, as it would need to be persisted on
// the enclosing config entry.
if len(args.Intention.Meta) > 0 {
return "", nil, fmt.Errorf("Meta must not be specified")
}
upsertEntry := args.Intention.ToConfigEntry(false)
return structs.ConfigEntryUpsertCAS, upsertEntry, nil
}
upsertEntry := prevEntry.Clone()
if len(args.Intention.Meta) > 0 {
// Meta is NOT permitted here, but there is one exception. If
// you are updating a previous record, but that record lives
// within a config entry that itself has Meta, then you may
// incidentally ship the Meta right back to consul.
//
// In that case if Meta is provided, it has to be a perfect
// match for what is already on the enclosing config entry so
// it's safe to discard.
if !equalStringMaps(upsertEntry.Meta, args.Intention.Meta) {
return "", nil, fmt.Errorf("Meta must not be specified, or should be unchanged during an update.")
}
// Now it is safe to discard
args.Intention.Meta = nil
}
sn := args.Intention.SourceServiceName()
upsertEntry.UpsertSourceByName(sn, args.Intention.ToSourceIntention(false))
return structs.ConfigEntryUpsertCAS, upsertEntry, nil
}
func (s *Intention) computeApplyChangesLegacyDelete(
accessorID string,
authz acl.Authorizer,
entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest,
) (structs.ConfigEntryOp, *structs.ServiceIntentionsConfigEntry, error) {
_, prevEntry, ixn, err := s.srv.fsm.State().IntentionGet(nil, args.Intention.ID)
if err != nil {
return "", nil, fmt.Errorf("Intention lookup failed: %v", err)
}
if ixn == nil || prevEntry == nil {
return "", nil, fmt.Errorf("Cannot delete non-existent intention: '%s'", args.Intention.ID)
}
if err := checkLegacyIntentionApplyAllowed(prevEntry); err != nil {
return "", nil, err
}
// Even though the eventual config entry RPC will do an authz check and
// validation, if we do them here too we can generate error messages that
// make more sense for legacy edits.
if !ixn.CanWrite(authz) {
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Deletion operation on intention denied due to ACLs", "intention", args.Intention.ID, "accessorID", accessorID)
return "", nil, acl.ErrPermissionDenied
}
upsertEntry := prevEntry.Clone()
deleted := upsertEntry.DeleteSourceByLegacyID(args.Intention.ID)
if !deleted {
return "", nil, fmt.Errorf("Cannot delete non-existent intention: '%s'", args.Intention.ID)
}
if upsertEntry == nil || len(upsertEntry.Sources) == 0 {
return structs.ConfigEntryDelete, &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: prevEntry.Name,
EnterpriseMeta: prevEntry.EnterpriseMeta,
}, nil
}
return structs.ConfigEntryUpsertCAS, upsertEntry, nil
}
func (s *Intention) computeApplyChangesDelete(
entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest,
) (structs.ConfigEntryOp, *structs.ServiceIntentionsConfigEntry, error) {
args.Intention.DefaultNamespaces(entMeta)
prevEntry, err := s.getServiceIntentionsConfigEntry(args.Intention.DestinationName, args.Intention.DestinationEnterpriseMeta())
if err != nil {
return "", nil, err
}
if prevEntry == nil {
return "", nil, nil // no op means no-op
}
// NOTE: validation errors may be misleading!
upsertEntry := prevEntry.Clone()
sn := args.Intention.SourceServiceName()
deleted := upsertEntry.DeleteSourceByName(sn)
if !deleted {
return "", nil, nil // no op means no-op
}
if upsertEntry == nil || len(upsertEntry.Sources) == 0 {
return structs.ConfigEntryDelete, &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: prevEntry.Name,
EnterpriseMeta: prevEntry.EnterpriseMeta,
}, nil
}
return structs.ConfigEntryUpsertCAS, upsertEntry, nil
}
func checkLegacyIntentionApplyAllowed(prevEntry *structs.ServiceIntentionsConfigEntry) error {
if prevEntry == nil {
return nil
}
if prevEntry.LegacyIDFieldsAreAllSet() {
return nil
}
sn := prevEntry.DestinationServiceName()
return fmt.Errorf("cannot use legacy intention API to edit intentions with a destination of %q after editing them via a service-intentions config entry", sn.String())
} }
// Get returns a single intention by ID. // Get returns a single intention by ID.

View File

@ -59,6 +59,59 @@ func (e *ServiceIntentionsConfigEntry) DestinationServiceName() ServiceName {
return NewServiceName(e.Name, &e.EnterpriseMeta) return NewServiceName(e.Name, &e.EnterpriseMeta)
} }
func (e *ServiceIntentionsConfigEntry) UpdateSourceByLegacyID(legacyID string, update *SourceIntention) bool {
for i, src := range e.Sources {
if src.LegacyID == legacyID {
e.Sources[i] = update
return true
}
}
return false
}
func (e *ServiceIntentionsConfigEntry) UpsertSourceByName(sn ServiceName, upsert *SourceIntention) {
for i, src := range e.Sources {
if src.SourceServiceName() == sn {
e.Sources[i] = upsert
return
}
}
e.Sources = append(e.Sources, upsert)
}
func (e *ServiceIntentionsConfigEntry) DeleteSourceByLegacyID(legacyID string) bool {
for i, src := range e.Sources {
if src.LegacyID == legacyID {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
e.Sources = append(e.Sources[:i], e.Sources[i+1:]...)
if len(e.Sources) == 0 {
e.Sources = nil
}
return true
}
}
return false
}
func (e *ServiceIntentionsConfigEntry) DeleteSourceByName(sn ServiceName) bool {
for i, src := range e.Sources {
if src.SourceServiceName() == sn {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
e.Sources = append(e.Sources[:i], e.Sources[i+1:]...)
if len(e.Sources) == 0 {
e.Sources = nil
}
return true
}
}
return false
}
func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intention { func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intention {
meta := e.Meta meta := e.Meta
if src.LegacyID != "" { if src.LegacyID != "" {