Logo

Autoschematic

GitHub
Cluster Login

Designing the Address Space

The address space is the first real design decision. This is where your connector will describe a hierarchy, based on file paths, of resources that it can manage.

The ResourceAddress trait defines the mapping between a path (address) and a remote resource for your connector.

For example, the following addresses (paths) correspond to individual remote resources:

  • aws/s3/us-east-1/buckets/photo-backup.ron is an AWS S3 bucket in us-east-1 named "photo-backup"
  • aws/elb/us-east-1/load_balancers/backend-autoschematic-sh/listeners/backend.ron is a listener named "backend" for the AWS elastic load balancer named 'backend-autoschematic-sh' in the 'us-east-1' region...
  • github/ottercorp/backend/repository.ron is a Github repository named "backend" in the "ottercorp" org ...and so on.

Note: Things like the AWS VPC connector and its resources, which do not have known IDs until after creation, use a "virtual" address. See autoschematic-connector-aws/vpc/src/connector.rs for a reference implementation of physical-to-virtual address mappings for APIs like AWS VPC. This will be expanded on as its own section, but for now this section only discusses non-virtual ("physical") addresses. You may not need to worry about the distinction, depending on the API you're writing a connector for.

For those resources that can be uniquely named ahead-of-time, like S3 Buckets, address decoding is simple - all addresses are physical addresses, meaning they correspond uniquely to an account-wide resource:

pub enum S3ResourceAddress {
    Bucket { region: String, name: String },
}

impl ResourceAddress for S3ResourceAddress {
    fn to_path_buf(&self) -> PathBuf {
        match &self {
            S3ResourceAddress::Bucket { region, name } => {
                PathBuf::from(format!("aws/s3/{region}/buckets/{name}.ron"))
            }
        }
    }

    fn from_path(path: &Path) -> Result<Self, anyhow::Error> {
        let path_components: Vec<&str> = path.components().map(|s| s.as_os_str().to_str().unwrap()).collect();

        match path_components[..] {
            ["aws", "s3", region, "buckets", name] if name.ends_with(".ron") => {
                Ok(S3ResourceAddress::Bucket {
                    region: region.to_string(),
                    name: name.strip_suffix(".ron").unwrap().to_string(),
                })
            }
            _ => Err(invalid_addr_path(path)),
        }
    }
}

Good address schemes have a few properties:

  • One path points to only one remote object, and vice-versa.
  • to_path_buf() and from_path() are exact inverses.
  • The address includes everything needed to locate the object remotely.

Config files can be specified in the same address enum. The GitHub connector does just that:

pub enum GitHubResourceAddress {
    Config,
    Repository { owner: String, repo: String },
    BranchProtection { owner: String, repo: String, branch: String },
}

[...]
impl Connector for GithubConnector {
[...]
    async fn filter(&self, addr: &Path) -> Result<FilterResponse, anyhow::Error> {
        if let Ok(addr) = GitHubResourceAddress::from_path(addr) {
            match addr {
                GitHubResourceAddress::Config => Ok(FilterResponse::Config),
                _ => Ok(FilterResponse::Resource),
            }
        } else {
            Ok(FilterResponse::none())
        }
    }
[...]
}

Reference files:

Next: Resource Bodies and filter()