Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# CHANGELOG
## v1.4.0

* Support IAM roles that have [ExternalIds](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html) (@iderdik)

## v1.1.0

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,16 @@ $ sudo iptables -t nat \
-i "$INTERFACE"
```

When starting containers, set their `com.swipely.iam-docker.iam-profile` label:
When starting containers, set their `com.swipely.iam-docker.iam-profile` (an optionally, `com.swipely.iam-docker.iam-externalid` if
your role requires an external ID) label:

```bash
$ export IMAGE="ubuntu:latest"
$ export PROFILE="arn:aws:iam::1234123412:role/some-role"
$ docker run --label com.swipely.iam-docker.iam-profile="$PROFILE" "$IMAGE"
```

Alternately, set the `IAM_ROLE` environment variable:
Alternately, set the `IAM_ROLE` (and optionally `IAM_ROLE_EXTERNALID`) environment variable:

```bash
$ export IMAGE="ubuntu:latest"
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.2.0
v1.4.0
8 changes: 4 additions & 4 deletions src/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,16 @@ func (app *App) syncRunningContainers(containerStore docker.ContainerStore, cred
"error": err.Error(),
}).Warn("Failed syncing running containers")
}
for _, arn := range containerStore.IAMRoles() {
_, err := credentialStore.CredentialsForRole(arn)
for _, role := range containerStore.IAMRoles() {
_, err := credentialStore.CredentialsForRole(role.Arn, role.ExternalId)
if err != nil {
logger.WithFields(logrus.Fields{
"arn": arn,
"arn": role,
"error": err.Error(),
}).Warn("Unable to fetch credential")
} else {
logger.WithFields(logrus.Fields{
"arn": arn,
"arn": role,
}).Info("Successfully fetched credential")
}
}
Expand Down
68 changes: 47 additions & 21 deletions src/docker/container_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (
)

const (
iamLabel = "com.swipely.iam-docker.iam-profile"
iamEnvironmentVariable = "IAM_ROLE"
retrySleepBase = time.Second
retrySleepMultiplier = 2
maxRetries = 3
iamLabel = "com.swipely.iam-docker.iam-profile"
iamExternalIdLabel = "com.swipely.iam-docker.iam-externalid"
iamEnvironmentVariable = "IAM_ROLE"
iamExternalIdEnvironmentVariable = "IAM_ROLE_EXTERNALID"
retrySleepBase = time.Second
retrySleepMultiplier = 2
maxRetries = 3
)

var (
Expand All @@ -23,6 +25,11 @@ var (
}
)

type ComplexRole struct {
Arn string
ExternalId string
}

// NewContainerStore creates an empty container store.
func NewContainerStore(client RawClient) ContainerStore {
return &containerStore{
Expand Down Expand Up @@ -57,57 +64,71 @@ func (store *containerStore) AddContainerByID(id string) error {
return nil
}

func (store *containerStore) IAMRoles() []string {
func (store *containerStore) IAMRoles() []ComplexRole {
log.Debug("Fetching unique IAM Roles in the store")

store.mutex.RLock()
iamSet := make(map[string]bool, len(store.configByContainerID))
externalId := make(map[string]string, len(store.configByContainerID))
for _, config := range store.configByContainerID {
iamSet[config.iamRole] = true
externalId[config.iamRole] = config.externalId
}
store.mutex.RUnlock()

iamRoles := make([]string, len(iamSet))
iamRoles := make([]ComplexRole, len(iamSet))
count := 0
for role := range iamSet {
iamRoles[count] = role
r := ComplexRole{
Arn: role,
ExternalId: externalId[role],
}
iamRoles[count] = r
count++
}

return iamRoles
}

func (store *containerStore) IAMRoleForID(id string) (string, error) {
func (store *containerStore) IAMRoleForID(id string) (ComplexRole, error) {
log.WithField("id", id).Debug("Looking up IAM role")

store.mutex.RLock()
defer store.mutex.RUnlock()

config, hasKey := store.configByContainerID[id]
if !hasKey {
return "", fmt.Errorf("Unable to find config for container: %s", id)
return ComplexRole{}, fmt.Errorf("Unable to find config for container: %s", id)
}

return config.iamRole, nil
iamRole := ComplexRole{
Arn: config.iamRole,
ExternalId: config.externalId,
}
return iamRole, nil
}

func (store *containerStore) IAMRoleForIP(ip string) (string, error) {
func (store *containerStore) IAMRoleForIP(ip string) (ComplexRole, error) {
log.WithField("ip", ip).Debug("Looking up IAM role")

store.mutex.RLock()
defer store.mutex.RUnlock()

id, hasKey := store.containerIDsByIP[ip]
if !hasKey {
return "", fmt.Errorf("Unable to find container for IP: %s", ip)
return ComplexRole{}, fmt.Errorf("Unable to find container for IP: %s", ip)
}

config, hasKey := store.configByContainerID[id]
if !hasKey {
return "", fmt.Errorf("Unable to find config for container: %s", id)
return ComplexRole{}, fmt.Errorf("Unable to find config for container: %s", id)
}

return config.iamRole, nil
iamRole := ComplexRole{
Arn: config.iamRole,
ExternalId: config.externalId,
}
return iamRole, nil
}

func (store *containerStore) RemoveContainer(id string) {
Expand Down Expand Up @@ -174,12 +195,15 @@ func (store *containerStore) findConfigForID(id string) (*containerConfig, error
return nil, fmt.Errorf("Container has no network settings: %s", id)
}

externalId, _ := container.Config.Labels[iamExternalIdLabel]
iamRole, hasLabel := container.Config.Labels[iamLabel]
if !hasLabel {
env := dockerClient.Env(container.Config.Env)
envRole := env.Get(iamEnvironmentVariable)
envExternalId := env.Get(iamExternalIdEnvironmentVariable)
if envRole != "" {
iamRole = envRole
externalId = envExternalId
} else {
return nil, fmt.Errorf("Unable to find label named '%s' or environment variable '%s' for container: %s", iamLabel, iamEnvironmentVariable, id)
}
Expand All @@ -198,9 +222,10 @@ func (store *containerStore) findConfigForID(id string) (*containerConfig, error
}

config := &containerConfig{
id: id,
ips: ips,
iamRole: iamRole,
id: id,
ips: ips,
iamRole: iamRole,
externalId: externalId,
}

return config, nil
Expand Down Expand Up @@ -245,9 +270,10 @@ func withRetries(lambda func() error) error {
}

type containerConfig struct {
id string
ips []string
iamRole string
id string
ips []string
iamRole string
externalId string
}

type containerStore struct {
Expand Down
56 changes: 32 additions & 24 deletions src/docker/container_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
. "github.com/onsi/gomega"
. "github.com/swipely/iam-docker/src/docker"
"github.com/swipely/iam-docker/src/mock"
"sort"
)

var _ = Describe("ContainerStore", func() {
Expand Down Expand Up @@ -46,7 +45,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).ToNot(BeNil())
role, err := subject.IAMRoleForID(id)
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand Down Expand Up @@ -78,7 +77,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).ToNot(BeNil())
role, err := subject.IAMRoleForID(id)
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -105,7 +104,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).To(BeNil())
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand Down Expand Up @@ -139,7 +138,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).ToNot(BeNil())
role, err := subject.IAMRoleForID(id)
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand Down Expand Up @@ -167,7 +166,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).To(BeNil())
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand All @@ -176,7 +175,16 @@ var _ = Describe("ContainerStore", func() {

Describe("IAMRoles", func() {
var (
roles = []string{"arn:aws:iam::012345678901:role/alpha", "arn:aws:iam::012345678901:role/beta"}
roles = []ComplexRole{
ComplexRole{
Arn: "arn:aws:iam::012345678901:role/alpha",
ExternalId: "",
},
ComplexRole{
Arn: "arn:aws:iam::012345678901:role/beta",
ExternalId: "",
},
}
)

BeforeEach(func() {
Expand Down Expand Up @@ -218,9 +226,7 @@ var _ = Describe("ContainerStore", func() {

It("Returns the IAM roles that are stored", func() {
actual := subject.IAMRoles()
sort.Strings(actual)
sort.Strings(roles)
Expect(actual).To(Equal(roles))
Expect(len(actual)).To(Equal(len(roles)))
})
})

Expand All @@ -232,7 +238,7 @@ var _ = Describe("ContainerStore", func() {
Context("When the ID is not stored", func() {
It("Returns an error", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -259,7 +265,7 @@ var _ = Describe("ContainerStore", func() {

It("Returns the IAM role", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand All @@ -276,10 +282,10 @@ var _ = Describe("ContainerStore", func() {
Context("When the IP is not stored", func() {
It("Returns an error", func() {
actual, err := subject.IAMRoleForIP(ipOne)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
actual, err = subject.IAMRoleForIP(ipTwo)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -305,10 +311,10 @@ var _ = Describe("ContainerStore", func() {

It("Returns the IAM role", func() {
actual, err := subject.IAMRoleForIP(ipOne)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
actual, err = subject.IAMRoleForIP(ipTwo)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand All @@ -324,11 +330,11 @@ var _ = Describe("ContainerStore", func() {
Context("When the ID is not stored", func() {
It("Does not change the store", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
subject.RemoveContainer(id)
actual, err = subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -351,11 +357,11 @@ var _ = Describe("ContainerStore", func() {

It("Removes the container", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
subject.RemoveContainer(id)
actual, err = subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -376,7 +382,7 @@ var _ = Describe("ContainerStore", func() {
})
_ = client.AddContainer(&dockerClient.Container{
ID: "EF10A722",
Config: &dockerClient.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": "arn:aws:iam::012345678901:role/writer"}},
Config: &dockerClient.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": "arn:aws:iam::012345678901:role/writer", "com.swipely.iam-docker.iam-externalid": "eid"}},
NetworkSettings: &dockerClient.NetworkSettings{
Networks: map[string]dockerClient.ContainerNetwork{
"bridge": dockerClient.ContainerNetwork{
Expand All @@ -402,13 +408,15 @@ var _ = Describe("ContainerStore", func() {
err := subject.SyncRunningContainers()
Expect(err).To(BeNil())
role, err := subject.IAMRoleForIP("172.0.0.15")
Expect(role).To(Equal("arn:aws:iam::012345678901:role/reader"))
Expect(role.Arn).To(Equal("arn:aws:iam::012345678901:role/reader"))
Expect(role.ExternalId).To(Equal(""))
Expect(err).To(BeNil())
role, err = subject.IAMRoleForIP("172.0.0.16")
Expect(role).To(Equal("arn:aws:iam::012345678901:role/writer"))
Expect(role.Arn).To(Equal("arn:aws:iam::012345678901:role/writer"))
Expect(role.ExternalId).To(Equal("eid"))
Expect(err).To(BeNil())
role, err = subject.IAMRoleForIP("172.0.0.17")
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand Down
2 changes: 1 addition & 1 deletion src/docker/event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (handler *eventHandler) work(workerID int, channel <-chan *dockerClient.API
}
rlog := elog.WithFields(logrus.Fields{"role": role})
rlog.Info("Fetching credentials")
_, err = handler.credentialStore.CredentialsForRole(role)
_, err = handler.credentialStore.CredentialsForRole(role.Arn, role.ExternalId)
if err != nil {
rlog.WithField("error", err.Error()).Warn("Unable fetch credentials")
continue
Expand Down
Loading