Skip to content

Commit

Permalink
Merge branch 'feature/ecs-domainless-gmsa' into gmsa_linux_domainless…
Browse files Browse the repository at this point in the history
…_capability
  • Loading branch information
saikiranakula-amzn authored May 8, 2023
2 parents 7175a90 + 2a07541 commit 8fa37d5
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 22 deletions.
41 changes: 40 additions & 1 deletion agent/api/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ const (

// neuronVisibleDevicesEnvVar is the env which indicates that the container wants to use inferentia devices.
neuronVisibleDevicesEnvVar = "AWS_NEURON_VISIBLE_DEVICES"

credentialSpecPrefix = "credentialspec"

credentialSpecDomainlessPrefix = credentialSpecPrefix + "domainless"
)

var (
Expand Down Expand Up @@ -199,6 +203,8 @@ type Container struct {
Overrides ContainerOverrides `json:"overrides"`
// DockerConfig is the configuration used to create the container
DockerConfig DockerConfig `json:"dockerConfig"`
// CredentialSpecs is the configuration used for configuring gMSA authentication for the container
CredentialSpecs []string `json:"credentialSpecs,omitempty"`
// RegistryAuthentication is the auth data used to pull image
RegistryAuthentication *RegistryAuthenticationData `json:"registryAuthentication"`
// HealthCheckType is the mechanism to use for the container health check
Expand Down Expand Up @@ -1350,6 +1356,22 @@ func (c *Container) GetCredentialSpec() (string, error) {
}

func (c *Container) getCredentialSpec() (string, error) {
credSpecHostConfig, err := c.getCredentialSpecFromHostConfig()
credSpecCredentialSpecsContainerField, err2 := c.getCredentialSpecFromCredentialSpecsContainerField()

// Prefer to use CredentialSpecsContainerField because of the upcoming docker runtime deprecation
if err2 == nil {
return credSpecCredentialSpecsContainerField, nil
}

if err == nil {
return credSpecHostConfig, nil
}

return "", errors.New("unable to obtain credentialspec from both hostConfig and credentialSpecs")
}

func (c *Container) getCredentialSpecFromHostConfig() (string, error) {
c.lock.RLock()
defer c.lock.RUnlock()

Expand All @@ -1364,14 +1386,31 @@ func (c *Container) getCredentialSpec() (string, error) {
}

for _, opt := range hostConfig.SecurityOpt {
if strings.HasPrefix(opt, "credentialspec") {
if strings.HasPrefix(opt, credentialSpecPrefix) {
return opt, nil
}
}

return "", errors.New("unable to obtain credentialspec")
}

func (c *Container) getCredentialSpecFromCredentialSpecsContainerField() (string, error) {
c.lock.RLock()
defer c.lock.RUnlock()

if c.CredentialSpecs == nil || len(c.CredentialSpecs) == 0 {
return "", errors.New("empty container credentialSpecs")
}

for _, credentialSpec := range c.CredentialSpecs {
if strings.HasPrefix(credentialSpec, credentialSpecPrefix) || strings.HasPrefix(credentialSpec, credentialSpecDomainlessPrefix) {
return credentialSpec, nil
}
}

return "", errors.New("credentialspec not found in CredentialSpecs field")
}

func (c *Container) GetManagedAgentStatus(agentName string) apicontainerstatus.ManagedAgentStatus {
c.lock.RLock()
defer c.lock.RUnlock()
Expand Down
161 changes: 140 additions & 21 deletions agent/api/container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -984,32 +984,82 @@ func TestRequiresCredentialSpec(t *testing.T) {
},
{
name: "invalid_case",
container: getContainer("invalid"),
container: getContainer("invalid", nil),
expectedOutput: false,
},
{
name: "empty_sec_opt",
container: getContainer("{\"NetworkMode\":\"bridge\"}"),
container: getContainer("{\"NetworkMode\":\"bridge\"}", nil),
expectedOutput: false,
},
{
name: "missing_credentialspec",
container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"),
container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}", nil),
expectedOutput: false,
},
{
name: "wrong_prefix",
container: getContainer("{\"SecurityOpt\": [\"credential2spec:file://gmsa_gmsa-acct.json\"]}", nil),
expectedOutput: false,
},
{
name: "valid_credentialspec_file",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}"),
container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}", nil),
expectedOutput: true,
},
{
name: "valid_credentialspec_s3",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}\"]}"),
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}\"]}", nil),
expectedOutput: true,
},
{
name: "valid_credentialspec_ssm",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/parameter_name\"]}"),
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/parameter_name\"]}", nil),
expectedOutput: true,
},
{
name: "valid_credentialspec_file_not_hostconfig",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspec:file://gmsa-acct.json"}),
expectedOutput: true,
},
{
name: "valid_credentialspec_file_domainless_not_hostconfig",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:file://gmsa-acct.json"}),
expectedOutput: true,
},
{
name: "valid_credentialspec_s3_not_hostconfig",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}"}),
expectedOutput: true,
},
{
name: "valid_credentialspec_s3_domainless_not_hostconfig",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:arn:aws:s3:::${BucketName}/${ObjectName}"}),
expectedOutput: true,
},
{
name: "valid_credentialspec_ssm_not_hostconfig",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:arn:aws:ssm:region:aws_account_id:parameter/parameter_name"}),
expectedOutput: true,
},
{
name: "valid_credentialspec_ssm_domainless_not_hostconfig",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:arn:aws:ssm:region:aws_account_id:parameter/parameter_name"}),
expectedOutput: true,
},
{
name: "host_config_credentialspecs_file",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://hostconfig.json\"]}", []string{"credentialspecdomainless:file://credentialspecs.json"}),
expectedOutput: true,
},
{
name: "host_config_credentialspecs_s3",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::hostconfig.json\"]}", []string{"credentialspec:arn:aws:s3:::credentialspecs.json"}),
expectedOutput: true,
},
{
name: "host_config_credentialspecs_ssm",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/hostconfig.json\"]}", []string{"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/credentialspecs.json"}),
expectedOutput: true,
},
}
Expand All @@ -1032,25 +1082,31 @@ func TestGetCredentialSpecErr(t *testing.T) {
name: "hostconfig_nil",
container: &Container{},
expectedOutputString: "",
expectedErrorString: "empty container hostConfig",
expectedErrorString: "unable to obtain credentialspec from both hostConfig and credentialSpecs",
},
{
name: "invalid_case",
container: getContainer("invalid"),
container: getContainer("invalid", nil),
expectedOutputString: "",
expectedErrorString: "unable to obtain security options from container hostConfig",
expectedErrorString: "unable to obtain credentialspec from both hostConfig and credentialSpecs",
},
{
name: "empty_sec_opt",
container: getContainer("{\"NetworkMode\":\"bridge\"}"),
container: getContainer("{\"NetworkMode\":\"bridge\"}", nil),
expectedOutputString: "",
expectedErrorString: "unable to obtain security options from container hostConfig",
expectedErrorString: "unable to obtain credentialspec from both hostConfig and credentialSpecs",
},
{
name: "missing_credentialspec",
container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"),
container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}", nil),
expectedOutputString: "",
expectedErrorString: "unable to obtain credentialspec from both hostConfig and credentialSpecs",
},
{
name: "invalid_case_credential_specs",
container: getContainer("{\"NetworkMode\":\"bridge\"}", []string{"invalid"}),
expectedOutputString: "",
expectedErrorString: "unable to obtain credentialspec",
expectedErrorString: "unable to obtain credentialspec from both hostConfig and credentialSpecs",
},
}

Expand All @@ -1063,20 +1119,83 @@ func TestGetCredentialSpecErr(t *testing.T) {
}
}

func TestGetCredentialSpecHappyPath(t *testing.T) {
c := getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}")

expectedCredentialSpec := "credentialspec:file://gmsa_gmsa-acct.json"
func TestGetCredentialSpec(t *testing.T) {
testCases := []struct {
name string
container *Container
expectedOutputString string
}{
{
name: "hostconfig_file",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}", nil),
expectedOutputString: "credentialspec:file://gmsa_gmsa-acct.json",
},
{
name: "hostconfig_file_multiple",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\", \"credentialspec:file://gmsa_gmsa-acct2.json\"]}", nil),
expectedOutputString: "credentialspec:file://gmsa_gmsa-acct.json",
},
{
name: "hostconfig_s3",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}\"]}", nil),
expectedOutputString: "credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}",
},
{
name: "hostconfig_ssm",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/parameter_name\"]}", nil),
expectedOutputString: "credentialspec:arn:aws:ssm:region:aws_account_id:parameter/parameter_name",
},
{
name: "credentialspecs_file",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:file://gmsa_gmsa-acct.json"}),
expectedOutputString: "credentialspecdomainless:file://gmsa_gmsa-acct.json",
},
{
name: "credentialspecs_file_multiple",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:file://gmsa_gmsa-acct.json", "credentialspecdomainless:file://gmsa_gmsa-acct2.json"}),
expectedOutputString: "credentialspecdomainless:file://gmsa_gmsa-acct.json",
},
{
name: "credentialspecs_s3",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:arn:aws:s3:::${BucketName}/${ObjectName}"}),
expectedOutputString: "credentialspecdomainless:arn:aws:s3:::${BucketName}/${ObjectName}",
},
{
name: "credentialspecs_ssm",
container: getContainer("{\"SecurityOpt\": []}", []string{"credentialspecdomainless:arn:aws:ssm:region:aws_account_id:parameter/parameter_name"}),
expectedOutputString: "credentialspecdomainless:arn:aws:ssm:region:aws_account_id:parameter/parameter_name",
},
{
name: "host_config_credentialspecs_file",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://hostconfig.json\"]}", []string{"credentialspecdomainless:file://credentialspecs.json"}),
expectedOutputString: "credentialspecdomainless:file://credentialspecs.json",
},
{
name: "host_config_credentialspecs_s3",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::hostconfig.json\"]}", []string{"credentialspec:arn:aws:s3:::credentialspecs.json"}),
expectedOutputString: "credentialspec:arn:aws:s3:::credentialspecs.json",
},
{
name: "host_config_credentialspecs_ssm",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/hostconfig.json\"]}", []string{"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/credentialspecs.json"}),
expectedOutputString: "credentialspec:arn:aws:ssm:region:aws_account_id:parameter/credentialspecs.json",
},
}

credentialspec, err := c.GetCredentialSpec()
assert.NoError(t, err)
assert.EqualValues(t, expectedCredentialSpec, credentialspec)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expectedOutputStr, err := tc.container.GetCredentialSpec()
assert.NoError(t, err)
assert.Equal(t, tc.expectedOutputString, expectedOutputStr)
})
}
}

func getContainer(hostConfig string) *Container {
func getContainer(hostConfig string, credentialSpecs []string) *Container {
c := &Container{
Name: "c",
}
c.DockerConfig.HostConfig = &hostConfig
c.CredentialSpecs = credentialSpecs
return c
}

0 comments on commit 8fa37d5

Please sign in to comment.