server: intentions CRUD requires connect to be enabled (#9194)

Fixes #9123
This commit is contained in:
R.B. Boyer 2020-11-13 16:19:12 -06:00 committed by GitHub
parent 53a7f852fe
commit db1184c094
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 67 deletions

View File

@ -444,6 +444,11 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r
func (c *ConfigEntry) preflightCheck(kind string) error { func (c *ConfigEntry) preflightCheck(kind string) error {
switch kind { switch kind {
case structs.ServiceIntentions: case structs.ServiceIntentions:
// Exit early if Connect hasn't been enabled.
if !c.srv.config.ConnectEnabled {
return ErrConnectNotEnabled
}
usingConfigEntries, err := c.srv.fsm.State().AreIntentionsInConfigEntries() usingConfigEntries, err := c.srv.fsm.State().AreIntentionsInConfigEntries()
if err != nil { if err != nil {
return fmt.Errorf("system metadata lookup failed: %v", err) return fmt.Errorf("system metadata lookup failed: %v", err)

View File

@ -59,6 +59,11 @@ func (s *Intention) legacyUpgradeCheck() error {
// Apply creates or updates an intention in the data store. // Apply creates or updates an intention in the data store.
func (s *Intention) Apply(args *structs.IntentionRequest, reply *string) error { func (s *Intention) Apply(args *structs.IntentionRequest, reply *string) error {
// Exit early if Connect hasn't been enabled.
if !s.srv.config.ConnectEnabled {
return ErrConnectNotEnabled
}
// Ensure that all service-intentions config entry writes go to the primary // Ensure that all service-intentions config entry writes go to the primary
// datacenter. These will then be replicated to all the other datacenters. // datacenter. These will then be replicated to all the other datacenters.
args.Datacenter = s.srv.config.PrimaryDatacenter args.Datacenter = s.srv.config.PrimaryDatacenter
@ -402,9 +407,12 @@ func (s *Intention) computeApplyChangesDelete(
} }
// Get returns a single intention by ID. // Get returns a single intention by ID.
func (s *Intention) Get( func (s *Intention) Get(args *structs.IntentionQueryRequest, reply *structs.IndexedIntentions) error {
args *structs.IntentionQueryRequest, // Exit early if Connect hasn't been enabled.
reply *structs.IndexedIntentions) error { if !s.srv.config.ConnectEnabled {
return ErrConnectNotEnabled
}
// Forward if necessary // Forward if necessary
if done, err := s.srv.ForwardRPC("Intention.Get", args, args, reply); done { if done, err := s.srv.ForwardRPC("Intention.Get", args, args, reply); done {
return err return err
@ -477,9 +485,12 @@ func (s *Intention) Get(
} }
// List returns all the intentions. // List returns all the intentions.
func (s *Intention) List( func (s *Intention) List(args *structs.IntentionListRequest, reply *structs.IndexedIntentions) error {
args *structs.IntentionListRequest, // Exit early if Connect hasn't been enabled.
reply *structs.IndexedIntentions) error { if !s.srv.config.ConnectEnabled {
return ErrConnectNotEnabled
}
// Forward if necessary // Forward if necessary
if done, err := s.srv.ForwardRPC("Intention.List", args, args, reply); done { if done, err := s.srv.ForwardRPC("Intention.List", args, args, reply); done {
return err return err
@ -544,9 +555,12 @@ func (s *Intention) List(
} }
// Match returns the set of intentions that match the given source/destination. // Match returns the set of intentions that match the given source/destination.
func (s *Intention) Match( func (s *Intention) Match(args *structs.IntentionQueryRequest, reply *structs.IndexedIntentionMatches) error {
args *structs.IntentionQueryRequest, // Exit early if Connect hasn't been enabled.
reply *structs.IndexedIntentionMatches) error { if !s.srv.config.ConnectEnabled {
return ErrConnectNotEnabled
}
// Forward if necessary // Forward if necessary
if done, err := s.srv.ForwardRPC("Intention.Match", args, args, reply); done { if done, err := s.srv.ForwardRPC("Intention.Match", args, args, reply); done {
return err return err
@ -615,9 +629,12 @@ func (s *Intention) Match(
// Note: Whenever the logic for this method is changed, you should take // Note: Whenever the logic for this method is changed, you should take
// a look at the agent authorize endpoint (agent/agent_endpoint.go) since // a look at the agent authorize endpoint (agent/agent_endpoint.go) since
// the logic there is similar. // the logic there is similar.
func (s *Intention) Check( func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.IntentionQueryCheckResponse) error {
args *structs.IntentionQueryRequest, // Exit early if Connect hasn't been enabled.
reply *structs.IntentionQueryCheckResponse) error { if !s.srv.config.ConnectEnabled {
return ErrConnectNotEnabled
}
// Forward maybe // Forward maybe
if done, err := s.srv.ForwardRPC("Intention.Check", args, args, reply); done { if done, err := s.srv.ForwardRPC("Intention.Check", args, args, reply); done {
return err return err

View File

@ -1017,18 +1017,32 @@ func (s *Server) bootstrapConfigEntries(entries []structs.ConfigEntry) error {
state := s.fsm.State() state := s.fsm.State()
// Do a quick preflight check to see if someone is trying to upgrade from // Do some quick preflight checks to see if someone is doing something
// an older pre-1.9.0 version of consul with intentions AND are trying to // that's not allowed at this time:
// bootstrap a service-intentions config entry at the same time. //
// - Trying to upgrade from an older pre-1.9.0 version of consul with
// intentions AND are trying to bootstrap a service-intentions config entry
// at the same time.
//
// - Trying to insert service-intentions config entries when connect is
// disabled.
usingConfigEntries, err := s.fsm.State().AreIntentionsInConfigEntries() usingConfigEntries, err := s.fsm.State().AreIntentionsInConfigEntries()
if err != nil { if err != nil {
return fmt.Errorf("Failed to determine if we are migrating intentions yet: %v", err) return fmt.Errorf("Failed to determine if we are migrating intentions yet: %v", err)
} }
if !usingConfigEntries {
if !usingConfigEntries || !s.config.ConnectEnabled {
for _, entry := range entries { for _, entry := range entries {
if entry.GetKind() == structs.ServiceIntentions { if entry.GetKind() == structs.ServiceIntentions {
return fmt.Errorf("Refusing to apply configuration entry %q / %q because intentions are still being migrated to config entries: %v", if !s.config.ConnectEnabled {
entry.GetKind(), entry.GetName(), err) return fmt.Errorf("Refusing to apply configuration entry %q / %q because Connect must be enabled to bootstrap intentions",
entry.GetKind(), entry.GetName())
}
if !usingConfigEntries {
return fmt.Errorf("Refusing to apply configuration entry %q / %q because intentions are still being migrated to config entries",
entry.GetKind(), entry.GetName())
}
} }
} }
} }

View File

@ -1230,63 +1230,133 @@ func TestLeader_ConfigEntryBootstrap(t *testing.T) {
func TestLeader_ConfigEntryBootstrap_Fail(t *testing.T) { func TestLeader_ConfigEntryBootstrap_Fail(t *testing.T) {
t.Parallel() t.Parallel()
pr, pw := io.Pipe() type testcase struct {
defer pw.Close() name string
entries []structs.ConfigEntry
serverCB func(c *Config)
expectMessage string
}
ch := make(chan string, 1) cases := []testcase{
go func() { {
defer pr.Close() name: "service-splitter without L7 protocol",
scan := bufio.NewScanner(pr) entries: []structs.ConfigEntry{
for scan.Scan() { &structs.ServiceSplitterConfigEntry{
line := scan.Text() Kind: structs.ServiceSplitter,
Name: "web",
if strings.Contains(line, "failed to establish leadership") { Splits: []structs.ServiceSplit{
ch <- "" {Weight: 100, Service: "web"},
return },
} },
if strings.Contains(line, "successfully established leadership") {
ch <- "leadership should not have gotten here if config entries properly failed"
return
}
}
if scan.Err() != nil {
ch <- fmt.Sprintf("ERROR: %v", scan.Err())
} else {
ch <- "should not get here"
}
}()
_, config := testServerConfig(t)
config.Build = "1.6.0"
config.ConfigEntryBootstrap = []structs.ConfigEntry{
&structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter,
Name: "web",
Splits: []structs.ServiceSplit{
{Weight: 100, Service: "web"},
}, },
expectMessage: `Failed to apply configuration entry "service-splitter" / "web": discovery chain "web" uses a protocol "tcp" that does not permit advanced routing or splitting behavior"`,
},
{
name: "service-intentions without migration",
entries: []structs.ConfigEntry{
&structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "web",
Sources: []*structs.SourceIntention{
{
Name: "debug",
Action: structs.IntentionActionAllow,
},
},
},
},
serverCB: func(c *Config) {
c.OverrideInitialSerfTags = func(tags map[string]string) {
tags["ft_si"] = "0"
}
},
expectMessage: `Refusing to apply configuration entry "service-intentions" / "web" because intentions are still being migrated to config entries`,
},
{
name: "service-intentions without Connect",
entries: []structs.ConfigEntry{
&structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "web",
Sources: []*structs.SourceIntention{
{
Name: "debug",
Action: structs.IntentionActionAllow,
},
},
},
},
serverCB: func(c *Config) {
c.ConnectEnabled = false
},
expectMessage: `Refusing to apply configuration entry "service-intentions" / "web" because Connect must be enabled to bootstrap intentions"`,
}, },
} }
logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{ for _, tc := range cases {
Name: config.NodeName, tc := tc
Level: hclog.Debug, t.Run(tc.name, func(t *testing.T) {
Output: io.MultiWriter(pw, testutil.NewLogBuffer(t)), pr, pw := io.Pipe()
}) defer pw.Close()
deps := newDefaultDeps(t, config) var (
deps.Logger = logger ch = make(chan string, 1)
applyErrorLine string
)
go func() {
defer pr.Close()
scan := bufio.NewScanner(pr)
for scan.Scan() {
line := scan.Text()
srv, err := NewServer(config, deps) if strings.Contains(line, "failed to establish leadership") {
require.NoError(t, err) applyErrorLine = line
defer srv.Shutdown() ch <- ""
return
}
if strings.Contains(line, "successfully established leadership") {
ch <- "leadership should not have gotten here if config entries properly failed"
return
}
}
select { if scan.Err() != nil {
case result := <-ch: ch <- fmt.Sprintf("ERROR: %v", scan.Err())
require.Empty(t, result) } else {
case <-time.After(time.Second): ch <- "should not get here"
t.Fatal("timeout waiting for a result from tailing logs") }
}()
_, config := testServerConfig(t)
config.Build = "1.6.0"
config.ConfigEntryBootstrap = tc.entries
if tc.serverCB != nil {
tc.serverCB(config)
}
logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{
Name: config.NodeName,
Level: hclog.Debug,
Output: io.MultiWriter(pw, testutil.NewLogBuffer(t)),
})
deps := newDefaultDeps(t, config)
deps.Logger = logger
srv, err := NewServer(config, deps)
require.NoError(t, err)
defer srv.Shutdown()
select {
case result := <-ch:
require.Empty(t, result)
if tc.expectMessage != "" {
require.Contains(t, applyErrorLine, tc.expectMessage)
}
case <-time.After(time.Second):
t.Fatal("timeout waiting for a result from tailing logs")
}
})
} }
} }