Logo

Autoschematic

GitHub
Cluster Login

Read Methods (get(), list(), and subpaths())

Connector::get(addr) should decode addr and make an API call or similar to the remote service in order to get the current state of the resource at addr, if it exists.

Connector::get(addr) should return the serialized (RON, JSON, YAML etc) state of the resource, and can return some other metadata as well in GetResourceResponse.

#[derive(Debug, Serialize, Deserialize)]
/// GetResourceResponse represents the successful result of Connector.get(addr),
/// where a resource exists at `addr` that is fetched by the connector.
pub struct GetResourceResponse {
    /// The on-disk (serialized) representation of this resource.
    pub resource_definition: Vec<u8>,
    /// The virtual address (if a canonical one can be derived) of this resource.
    /// E.G. a Grafana dashboard's physical address is based on its uid, but its virtual address on
    /// import might be hinted based on the dashboard's title.
    /// AWS resources might read a `name` (or configurable) tag to suggest a virtual address here on import,
    pub virt_addr: Option<PathBuf>,
    /// Outputs (e.g. canonical resource id, vpc_id, subnet_id...) to set here.
    pub outputs: Option<OutputMap>,
}
impl Connector for EcrConnector {
    [...]
    pub async fn get(&self, addr: &Path) -> Result<Option<GetResourceResponse>, anyhow::Error> {
        let addr = EcrResourceAddress::from_path(addr)?;

        match addr {
            [...]
            EcrResourceAddress::RepositoryPolicy { region, name } => {
                let client = self.get_or_init_client(&region).await?;

                // Get repository policy
                let policy_resp = client.get_repository_policy().repository_name(&name).send().await;

                match policy_resp {
                    Ok(policy_data) => {
                        if let Some(policy_text) = policy_data.policy_text {
                            // Parse policy JSON into RON value
                            let val: serde_json::Value = serde_json::from_str(&policy_text)?;
                            let rval: ron::Value = RON.from_str(&RON.to_string(&val)?)?;

                            let repo_policy = RepositoryPolicy { policy_document: rval };

                            return Ok(Some(GetResourceResponse {
                                resource_definition: EcrResource::RepositoryPolicy(repo_policy).to_bytes()?,
                                virt_addr: None,
                                outputs: None,
                            }));
                        }
                        Ok(None)
                    }
                    Err(e) => {
                        match e.into_service_error() {
                            aws_sdk_ecr::operation::get_repository_policy::GetRepositoryPolicyError::RepositoryNotFoundException(_) => Ok(None),
                            aws_sdk_ecr::operation::get_repository_policy::GetRepositoryPolicyError::RepositoryPolicyNotFoundException(_) => Ok(None),
                            e => Err(e.into())
                        }
                    }
                }
            }
            EcrResourceAddress::LifecyclePolicy { region, name } => {
                [...]
            }
            [...]
        }
        [...]
    }
}

Connector::subpaths() is optional, and used to constrain Connector::list(subpath). If Connector::subpaths() is not overridden, the default is to return ["./"], a single subpath that doesn't constrain Connector::list() at all. Connector::list(subpath) optionally interprets subpath to scan and list only a subset of resources, those that start with subpath. A utility function, addr_matches_filter(addr, filter) exists to do this subpath filtering in a path-normalized manner that also supports globs/wildcards.

Note that even if your connector ignores subpath, the host will filter results after the fact according to subpath if desired, so implementing it is only necessary to speed up and have finer granularity for imports. It's not necessary to implement it when just getting started.

async fn subpaths(&self) -> anyhow::Result<Vec<PathBuf>> {
    let mut res = Vec::new();

    for region in &self.config.read().await.enabled_regions {
        res.push(PathBuf::from(format!("aws/vpc/{}", region)));
    }

    Ok(res)
}
pub async fn do_list(&self, subpath: &Path) -> Result<Vec<PathBuf>, anyhow::Error> {
    let mut results = Vec::<PathBuf>::new();
    let config = self.config.read().await;

    for region_name in &config.enabled_regions {
        // Here, the connector restricts the scope of its list() operation to only those 
        // addresses that would fall under `subpath`, as long as `subpath` is of the form "aws/vpc/{region}"
        // for some enabled region.
        // This means the host can sensibly issue list operations in parallel, since we signalled with
        // Connector::subpaths() that we support interpreting all subpaths of that form to constrain our search and
        // avoid duplicated work.
        if !addr_matches_filter(&PathBuf::from(format!("aws/vpc/{}", region_name)), subpath) {
            continue;
        }
        let client = self.get_or_init_client(region_name).await.unwrap();
        [...]
    }
    [...]
}

get() returns serialized resource bytes plus optional outputs:

pub struct GetResourceResponse {
    pub resource_definition: Vec<u8>,
    pub outputs: Option<OutputMap>,
}

Important behavior:

  • Connector::get(addr) should return Ok(None) if the remote resource does not exist. Missing resources are not an error condition.
  • list() returns paths only. It does not need to load every resource body.
  • subpaths() should stay conservative. Return ./ if your connector cannot meaningfully split the address space.

Reference files:

Next: Planning and Executing Connector Ops