package deployer import ( "context" "fmt" "log/slog" "strconv" "time" "github.com/moby/moby/client" ) type Instance struct { ChallengeName string DeployKey string Address string AddressFormat string ExpiresAt time.Time } func (d *DockerDeployer) GetTeamInstances(ctx context.Context, team string) ([]Instance, error) { filters := client.Filters{} filters.Add("label", ContainerLabelForTeam+"="+team) containers, err := d.client.ContainerList(ctx, client.ContainerListOptions{ All: true, Filters: filters, }) if err != nil { return []Instance{}, err } var instances []Instance for _, c := range containers.Items { expiresAt, err := strconv.Atoi(c.Labels[ContainerLabelExpiresAt]) if err != nil { slog.Error("container has invalid expiry", "container", c.ID, "expiry", c.Labels[ContainerLabelExpiresAt]) continue } instances = append(instances, Instance{ ChallengeName: c.Labels[ContainerLabelChallenge], DeployKey: c.Labels[ContainerLabelDeployKey], Address: c.Labels[ContainerLabelAddress], AddressFormat: c.Labels[ContainerLabelAddressFormat], ExpiresAt: time.Unix(int64(expiresAt), 0), }) } return instances, nil } func (d *DockerDeployer) StopInstance(ctx context.Context, deployKey, team string) error { if deployKey == "" || team == "" { return fmt.Errorf("deploy key/team is invalid") } filters := client.Filters{} filters.Add("label", ContainerLabelForTeam+"="+team) filters.Add("label", ContainerLabelDeployKey+"="+deployKey) containers, err := d.client.ContainerList(ctx, client.ContainerListOptions{ All: true, Filters: filters, }) if err != nil { return fmt.Errorf("docker error") } if len(containers.Items) == 0 { return fmt.Errorf("no such instance") } for _, c := range containers.Items { _, err := d.client.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{ Force: true, }) if err != nil { slog.Error("failed to remove container", "container", c.ID, "cause", err) return fmt.Errorf("docker error") } slog.Info("container removed early", "container", c.ID) } networks, err := d.client.NetworkList(ctx, client.NetworkListOptions{ Filters: filters, }) if err != nil { return fmt.Errorf("docker error") } for _, n := range networks.Items { if err = d.forceRemoveNetwork(ctx, n.ID); err != nil { slog.Warn("failed to remove network", "network", n.ID, "cause", err) continue } slog.Info("network removed early", "network", n.ID) } return nil } func (d *DockerDeployer) RemoveExpiredResources(ctx context.Context) error { filters := client.Filters{} filters.Add("label", ContainerLabelManaged+"=yes") containers, err := d.client.ContainerList(ctx, client.ContainerListOptions{ All: true, Filters: filters, }) if err != nil { return err } for _, c := range containers.Items { expiry, err := strconv.ParseInt(c.Labels[ContainerLabelExpiresAt], 10, 64) if err != nil { slog.Warn("invalid timestamp on container label", "container", c.ID, "timestamp", c.Labels[ContainerLabelExpiresAt]) continue } if expiry > time.Now().Unix() { continue } _, err = d.client.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{ Force: true, }) if err != nil { return err } slog.Info("expired container removed", "container", c.ID) } networks, err := d.client.NetworkList(ctx, client.NetworkListOptions{ Filters: filters, }) if err != nil { return err } for _, n := range networks.Items { expiry, err := strconv.ParseInt(n.Labels[ContainerLabelExpiresAt], 10, 64) if err != nil { slog.Warn("invalid timestamp on network label", "network", n.ID, "timestamp", n.Labels[ContainerLabelExpiresAt]) continue } if expiry > time.Now().Unix() { continue } if err = d.forceRemoveNetwork(ctx, n.ID); err != nil { return err } slog.Info("expired network removed", "network", n.ID) } return nil } func (d *DockerDeployer) forceRemoveNetwork(ctx context.Context, networkID string) error { _, _ = d.client.NetworkDisconnect(ctx, networkID, client.NetworkDisconnectOptions{ Container: d.proxyContainerName, Force: true, }) _, err := d.client.NetworkRemove(ctx, networkID, client.NetworkRemoveOptions{}) if err != nil { return err } return nil }