mirror of https://github.com/crossplane/docs.git
674 lines
20 KiB
Markdown
674 lines
20 KiB
Markdown
---
|
|
title: Create a Configuration
|
|
weight: 4
|
|
---
|
|
|
|
In the [previous section] we were able to create a PostgreSQL database because
|
|
we had installed a configuration package that defined the `PostgreSQLInstance`
|
|
type and a `Composition` of managed resources that mapped to it. Crossplane
|
|
allows you to define your own composite resources (XRs) and compositions, then
|
|
package them up to be easily distributed as OCI images. This allows you to
|
|
construct a reproducible platform that exposes infrastructure APIs at your
|
|
desired level of abstraction, and can be installed into any Crossplane cluster.
|
|
|
|
## Create a Configuration Directory
|
|
|
|
We are going to build the same configuration package that we previously
|
|
installed. It will consist of three files:
|
|
|
|
* `crossplane.yaml` - Metadata about the configuration.
|
|
* `definition.yaml` - The XRD.
|
|
* `composition.yaml` - The Composition.
|
|
|
|
Crossplane can create a configuration from any directory with a valid
|
|
`crossplane.yaml` metadata file at its root, and one or more XRDs or
|
|
Compositions. The directory structure does not matter, as long as the
|
|
`crossplane.yaml` file is at the root. Note that a configuration need not
|
|
contain one XRD and one composition - it could include only an XRD, only a
|
|
composition, several compositions, or any combination thereof.
|
|
|
|
Before we go any further, we must create a directory in which to build our
|
|
configuration:
|
|
|
|
```console
|
|
mkdir crossplane-config
|
|
cd crossplane-config
|
|
```
|
|
|
|
We'll create the aforementioned three files in this directory, then build them
|
|
into a package.
|
|
|
|
> Note that `definition.yaml` and `composition.yaml` could be created directly
|
|
> in the Crossplane cluster without packaging them into a configuration. This
|
|
> can be useful for testing compositions before pushing them to a registry.
|
|
|
|
## Create CompositeResourceDefinition
|
|
|
|
First we'll create a `CompositeResourceDefinition` (XRD) to define the schema of
|
|
our `XPostgreSQLInstance` and its `PostgreSQLInstance` resource claim.
|
|
|
|
```yaml
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: CompositeResourceDefinition
|
|
metadata:
|
|
name: xpostgresqlinstances.database.example.org
|
|
spec:
|
|
group: database.example.org
|
|
names:
|
|
kind: XPostgreSQLInstance
|
|
plural: xpostgresqlinstances
|
|
claimNames:
|
|
kind: PostgreSQLInstance
|
|
plural: postgresqlinstances
|
|
connectionSecretKeys:
|
|
- username
|
|
- password
|
|
- endpoint
|
|
- port
|
|
versions:
|
|
- name: v1alpha1
|
|
served: true
|
|
referenceable: true
|
|
schema:
|
|
openAPIV3Schema:
|
|
type: object
|
|
properties:
|
|
spec:
|
|
type: object
|
|
properties:
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
storageGB:
|
|
type: integer
|
|
required:
|
|
- storageGB
|
|
required:
|
|
- parameters
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/definition.yaml
|
|
```
|
|
|
|
> You might notice that the XRD we created specifies both "names" and "claim
|
|
> names". This is because the composite resource it defines offers a composite
|
|
> resource claim (XRC).
|
|
|
|
## Create Compositions
|
|
|
|
Now we'll specify which managed resources our `XPostgreSQLInstance` XR
|
|
and its claim could be composed of, and how they should be configured. We do
|
|
this by defining a `Composition` that can satisfy the XR we defined above. In
|
|
this case, our `Composition` will specify how to provision a public PostgreSQL
|
|
instance on the chosen provider.
|
|
|
|
{{< tabs >}}
|
|
{{< tab "AWS (Default VPC)" >}}
|
|
|
|
> Note that this Composition will create an RDS instance using your default VPC,
|
|
> which may or may not allow connections from the internet depending on how it
|
|
> is configured. Select the AWS (New VPC) Composition if you wish to create an
|
|
> RDS instance that will allow traffic from the internet.
|
|
|
|
```yaml
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: Composition
|
|
metadata:
|
|
name: xpostgresqlinstances.aws.database.example.org
|
|
labels:
|
|
provider: aws
|
|
guide: quickstart
|
|
vpc: default
|
|
spec:
|
|
writeConnectionSecretsToNamespace: crossplane-system
|
|
compositeTypeRef:
|
|
apiVersion: database.example.org/v1alpha1
|
|
kind: XPostgreSQLInstance
|
|
resources:
|
|
- name: rdsinstance
|
|
base:
|
|
apiVersion: database.aws.crossplane.io/v1beta1
|
|
kind: RDSInstance
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
dbInstanceClass: db.t2.small
|
|
masterUsername: masteruser
|
|
engine: postgres
|
|
engineVersion: "12"
|
|
skipFinalSnapshotBeforeDeletion: true
|
|
publiclyAccessible: true
|
|
writeConnectionSecretToRef:
|
|
namespace: crossplane-system
|
|
patches:
|
|
- fromFieldPath: "metadata.uid"
|
|
toFieldPath: "spec.writeConnectionSecretToRef.name"
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "%s-postgresql"
|
|
- fromFieldPath: "spec.parameters.storageGB"
|
|
toFieldPath: "spec.forProvider.allocatedStorage"
|
|
connectionDetails:
|
|
- fromConnectionSecretKey: username
|
|
- fromConnectionSecretKey: password
|
|
- fromConnectionSecretKey: endpoint
|
|
- fromConnectionSecretKey: port
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/aws/composition.yaml
|
|
```
|
|
|
|
{{< /tab >}}
|
|
{{< tab "AWS (New VPC)" >}}
|
|
|
|
> Note: this `Composition` for AWS also includes several networking managed
|
|
> resources that are required to provision a publicly available PostgreSQL
|
|
> instance. Composition enables scenarios such as this, as well as far more
|
|
> complex ones. See the [composition] documentation for more information.
|
|
|
|
```yaml
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: Composition
|
|
metadata:
|
|
name: vpcpostgresqlinstances.aws.database.example.org
|
|
labels:
|
|
provider: aws
|
|
guide: quickstart
|
|
vpc: new
|
|
spec:
|
|
writeConnectionSecretsToNamespace: crossplane-system
|
|
compositeTypeRef:
|
|
apiVersion: database.example.org/v1alpha1
|
|
kind: XPostgreSQLInstance
|
|
resources:
|
|
- name: vpc
|
|
base:
|
|
apiVersion: ec2.aws.crossplane.io/v1beta1
|
|
kind: VPC
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
cidrBlock: 192.168.0.0/16
|
|
enableDnsSupport: true
|
|
enableDnsHostNames: true
|
|
- name: subnet-a
|
|
base:
|
|
apiVersion: ec2.aws.crossplane.io/v1beta1
|
|
kind: Subnet
|
|
metadata:
|
|
labels:
|
|
zone: us-east-1a
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
cidrBlock: 192.168.64.0/18
|
|
vpcIdSelector:
|
|
matchControllerRef: true
|
|
availabilityZone: us-east-1a
|
|
- name: subnet-b
|
|
base:
|
|
apiVersion: ec2.aws.crossplane.io/v1beta1
|
|
kind: Subnet
|
|
metadata:
|
|
labels:
|
|
zone: us-east-1b
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
cidrBlock: 192.168.128.0/18
|
|
vpcIdSelector:
|
|
matchControllerRef: true
|
|
availabilityZone: us-east-1b
|
|
- name: subnet-c
|
|
base:
|
|
apiVersion: ec2.aws.crossplane.io/v1beta1
|
|
kind: Subnet
|
|
metadata:
|
|
labels:
|
|
zone: us-east-1c
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
cidrBlock: 192.168.192.0/18
|
|
vpcIdSelector:
|
|
matchControllerRef: true
|
|
availabilityZone: us-east-1c
|
|
- name: dbsubnetgroup
|
|
base:
|
|
apiVersion: database.aws.crossplane.io/v1beta1
|
|
kind: DBSubnetGroup
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
description: An excellent formation of subnetworks.
|
|
subnetIdSelector:
|
|
matchControllerRef: true
|
|
- name: internetgateway
|
|
base:
|
|
apiVersion: ec2.aws.crossplane.io/v1beta1
|
|
kind: InternetGateway
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
vpcIdSelector:
|
|
matchControllerRef: true
|
|
- name: routetable
|
|
base:
|
|
apiVersion: ec2.aws.crossplane.io/v1beta1
|
|
kind: RouteTable
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
vpcIdSelector:
|
|
matchControllerRef: true
|
|
routes:
|
|
- destinationCidrBlock: 0.0.0.0/0
|
|
gatewayIdSelector:
|
|
matchControllerRef: true
|
|
associations:
|
|
- subnetIdSelector:
|
|
matchLabels:
|
|
zone: us-east-1a
|
|
- subnetIdSelector:
|
|
matchLabels:
|
|
zone: us-east-1b
|
|
- subnetIdSelector:
|
|
matchLabels:
|
|
zone: us-east-1c
|
|
- name: securitygroup
|
|
base:
|
|
apiVersion: ec2.aws.crossplane.io/v1beta1
|
|
kind: SecurityGroup
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
vpcIdSelector:
|
|
matchControllerRef: true
|
|
groupName: crossplane-getting-started
|
|
description: Allow access to PostgreSQL
|
|
ingress:
|
|
- fromPort: 5432
|
|
toPort: 5432
|
|
ipProtocol: tcp
|
|
ipRanges:
|
|
- cidrIp: 0.0.0.0/0
|
|
description: Everywhere
|
|
- name: rdsinstance
|
|
base:
|
|
apiVersion: database.aws.crossplane.io/v1beta1
|
|
kind: RDSInstance
|
|
spec:
|
|
forProvider:
|
|
region: us-east-1
|
|
dbSubnetGroupNameSelector:
|
|
matchControllerRef: true
|
|
vpcSecurityGroupIDSelector:
|
|
matchControllerRef: true
|
|
dbInstanceClass: db.t2.small
|
|
masterUsername: masteruser
|
|
engine: postgres
|
|
engineVersion: "12"
|
|
skipFinalSnapshotBeforeDeletion: true
|
|
publiclyAccessible: true
|
|
writeConnectionSecretToRef:
|
|
namespace: crossplane-system
|
|
patches:
|
|
- fromFieldPath: "metadata.uid"
|
|
toFieldPath: "spec.writeConnectionSecretToRef.name"
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "%s-postgresql"
|
|
- fromFieldPath: "spec.parameters.storageGB"
|
|
toFieldPath: "spec.forProvider.allocatedStorage"
|
|
connectionDetails:
|
|
- fromConnectionSecretKey: username
|
|
- fromConnectionSecretKey: password
|
|
- fromConnectionSecretKey: endpoint
|
|
- fromConnectionSecretKey: port
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/aws-with-vpc/composition.yaml
|
|
```
|
|
|
|
{{< /tab >}}
|
|
{{< tab "GCP" >}}
|
|
|
|
```yaml
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: Composition
|
|
metadata:
|
|
name: xpostgresqlinstances.gcp.database.example.org
|
|
labels:
|
|
provider: gcp
|
|
guide: quickstart
|
|
spec:
|
|
writeConnectionSecretsToNamespace: crossplane-system
|
|
compositeTypeRef:
|
|
apiVersion: database.example.org/v1alpha1
|
|
kind: XPostgreSQLInstance
|
|
resources:
|
|
- name: cloudsqlinstance
|
|
base:
|
|
apiVersion: database.gcp.crossplane.io/v1beta1
|
|
kind: CloudSQLInstance
|
|
spec:
|
|
forProvider:
|
|
databaseVersion: POSTGRES_12
|
|
region: us-central1
|
|
settings:
|
|
tier: db-custom-1-3840
|
|
dataDiskType: PD_SSD
|
|
ipConfiguration:
|
|
ipv4Enabled: true
|
|
authorizedNetworks:
|
|
- value: "0.0.0.0/0"
|
|
writeConnectionSecretToRef:
|
|
namespace: crossplane-system
|
|
patches:
|
|
- fromFieldPath: "metadata.uid"
|
|
toFieldPath: "spec.writeConnectionSecretToRef.name"
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "%s-postgresql"
|
|
- fromFieldPath: "spec.parameters.storageGB"
|
|
toFieldPath: "spec.forProvider.settings.dataDiskSizeGb"
|
|
connectionDetails:
|
|
- fromConnectionSecretKey: username
|
|
- fromConnectionSecretKey: password
|
|
- fromConnectionSecretKey: endpoint
|
|
- type: FromValue
|
|
name: port
|
|
value: "5432"
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/gcp/composition.yaml
|
|
```
|
|
|
|
{{< /tab >}}
|
|
{{< tab "Azure" >}}
|
|
|
|
> Note: the `Composition` for Azure also includes a `ResourceGroup` and
|
|
> `PostgreSQLServerFirewallRule` that are required to provision a publicly
|
|
> available PostgreSQL instance on Azure. Composition enables scenarios such as
|
|
> this, as well as far more complex ones. See the [composition] documentation
|
|
> for more information.
|
|
|
|
```yaml
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: Composition
|
|
metadata:
|
|
name: xpostgresqlinstances.azure.database.example.org
|
|
labels:
|
|
provider: azure
|
|
guide: quickstart
|
|
spec:
|
|
writeConnectionSecretsToNamespace: crossplane-system
|
|
compositeTypeRef:
|
|
apiVersion: database.example.org/v1alpha1
|
|
kind: XPostgreSQLInstance
|
|
resources:
|
|
- name: resourcegroup
|
|
base:
|
|
apiVersion: azure.crossplane.io/v1alpha3
|
|
kind: ResourceGroup
|
|
spec:
|
|
location: West US 2
|
|
- name: postgresqlserver
|
|
base:
|
|
apiVersion: database.azure.crossplane.io/v1beta1
|
|
kind: PostgreSQLServer
|
|
spec:
|
|
forProvider:
|
|
administratorLogin: myadmin
|
|
resourceGroupNameSelector:
|
|
matchControllerRef: true
|
|
location: West US 2
|
|
sslEnforcement: Disabled
|
|
version: "9.6"
|
|
sku:
|
|
tier: GeneralPurpose
|
|
capacity: 2
|
|
family: Gen5
|
|
writeConnectionSecretToRef:
|
|
namespace: crossplane-system
|
|
patches:
|
|
- fromFieldPath: "metadata.uid"
|
|
toFieldPath: "spec.writeConnectionSecretToRef.name"
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "%s-postgresql"
|
|
- fromFieldPath: "spec.parameters.storageGB"
|
|
toFieldPath: "spec.forProvider.storageProfile.storageMB"
|
|
transforms:
|
|
- type: math
|
|
math:
|
|
multiply: 1024
|
|
connectionDetails:
|
|
- fromConnectionSecretKey: username
|
|
- fromConnectionSecretKey: password
|
|
- fromConnectionSecretKey: endpoint
|
|
- type: FromValue
|
|
name: port
|
|
value: "5432"
|
|
- name: firewallrule
|
|
base:
|
|
apiVersion: database.azure.crossplane.io/v1alpha3
|
|
kind: PostgreSQLServerFirewallRule
|
|
spec:
|
|
forProvider:
|
|
serverNameSelector:
|
|
matchControllerRef: true
|
|
resourceGroupNameSelector:
|
|
matchControllerRef: true
|
|
properties:
|
|
startIpAddress: 0.0.0.0
|
|
endIpAddress: 255.255.255.254
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/azure/composition.yaml
|
|
```
|
|
|
|
{{< /tab >}}
|
|
{{< /tabs >}}
|
|
|
|
## Build and Push The Configuration
|
|
|
|
Finally, we'll author our metadata file then build and push our configuration
|
|
so that Crossplane users may install it.
|
|
|
|
> Note that Crossplane pushes packages to an OCI registry - currently [Docker
|
|
> Hub] by default. You may need to run `docker login` before you are able to
|
|
> push a package.
|
|
|
|
{{< tabs >}}
|
|
{{< tab "AWS (Default VPC)" >}}
|
|
|
|
```yaml
|
|
apiVersion: meta.pkg.crossplane.io/v1
|
|
kind: Configuration
|
|
metadata:
|
|
name: getting-started-with-aws
|
|
annotations:
|
|
guide: quickstart
|
|
provider: aws
|
|
vpc: default
|
|
spec:
|
|
crossplane:
|
|
version: ">=v1.4.0-0"
|
|
dependsOn:
|
|
- provider: xpkg.upbound.io/crossplane-contrib/provider-aws
|
|
version: ">=v0.18.2"
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/aws/crossplane.yaml
|
|
|
|
kubectl crossplane build configuration
|
|
```
|
|
|
|
You should see a file in your working directory with a `.xpkg` extension. The
|
|
Crossplane CLI will automatically tag and push it to the registry of your
|
|
choosing in the next step if it is the only `.xpkg` in the directory. Otherwise
|
|
you may specify a specific package by using the `-f` flag.
|
|
|
|
```console
|
|
# Set this to the Docker Hub username or OCI registry you wish to use.
|
|
REG=my-package-repo
|
|
kubectl crossplane push configuration ${REG}/getting-started-with-aws:v1.10.1
|
|
```
|
|
|
|
> Note that the Crossplane CLI will not follow symbolic links for files in the
|
|
> root package directory.
|
|
|
|
{{< /tab >}}
|
|
{{< tab "AWS (New VPC)" >}}
|
|
|
|
```yaml
|
|
apiVersion: meta.pkg.crossplane.io/v1
|
|
kind: Configuration
|
|
metadata:
|
|
name: getting-started-with-aws-with-vpc
|
|
annotations:
|
|
guide: quickstart
|
|
provider: aws
|
|
vpc: new
|
|
spec:
|
|
crossplane:
|
|
version: ">=v1.4.0-0"
|
|
dependsOn:
|
|
- provider: xpkg.upbound.io/crossplane-contrib/provider-aws
|
|
version: ">=v0.18.2"
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/aws-with-vpc/crossplane.yaml
|
|
|
|
kubectl crossplane build configuration
|
|
```
|
|
|
|
You should see a file in your working directory with a `.xpkg` extension. The
|
|
Crossplane CLI will automatically tag and push it to the registry of your
|
|
choosing in the next step if it is the only `.xpkg` in the directory. Otherwise
|
|
you may specify a specific package by using the `-f` flag.
|
|
|
|
```console
|
|
# Set this to the Docker Hub username or OCI registry you wish to use.
|
|
REG=my-package-repo
|
|
kubectl crossplane push configuration ${REG}/getting-started-with-aws-with-vpc:v1.10.1
|
|
```
|
|
|
|
> Note that the Crossplane CLI will not follow symbolic links for files in the
|
|
> root package directory.
|
|
|
|
{{< /tab >}}
|
|
{{< tab "GCP" >}}
|
|
|
|
```yaml
|
|
apiVersion: meta.pkg.crossplane.io/v1
|
|
kind: Configuration
|
|
metadata:
|
|
name: getting-started-with-gcp
|
|
annotations:
|
|
guide: quickstart
|
|
provider: gcp
|
|
spec:
|
|
crossplane:
|
|
version: ">=v1.4.0-0"
|
|
dependsOn:
|
|
- provider: xpkg.upbound.io/crossplane-contrib/provider-gcp
|
|
version: ">=v0.13.0"
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/gcp/crossplane.yaml
|
|
|
|
kubectl crossplane build configuration
|
|
```
|
|
|
|
You should see a file in your working directory with a `.xpkg` extension. The
|
|
Crossplane CLI will automatically tag and push it to the registry of your
|
|
choosing in the next step if it is the only `.xpkg` in the directory. Otherwise
|
|
you may specify a specific package by using the `-f` flag.
|
|
|
|
```console
|
|
# Set this to the Docker Hub username or OCI registry you wish to use.
|
|
REG=my-package-repo
|
|
kubectl crossplane push configuration ${REG}/getting-started-with-gcp:v1.10.1
|
|
```
|
|
|
|
> Note that the Crossplane CLI will not follow symbolic links for files in the
|
|
> root package directory.
|
|
|
|
{{< /tab >}}
|
|
{{< tab "Azure" >}}
|
|
|
|
```yaml
|
|
apiVersion: meta.pkg.crossplane.io/v1
|
|
kind: Configuration
|
|
metadata:
|
|
name: getting-started-with-azure
|
|
annotations:
|
|
guide: quickstart
|
|
provider: azure
|
|
spec:
|
|
crossplane:
|
|
version: ">=v1.4.0-0"
|
|
dependsOn:
|
|
- provider: xpkg.upbound.io/crossplane-contrib/provider-azure
|
|
version: ">=v0.13.0"
|
|
```
|
|
|
|
```console
|
|
curl -OL https://raw.githubusercontent.com/crossplane/crossplane/master/docs/snippets/package/azure/crossplane.yaml
|
|
|
|
kubectl crossplane build configuration
|
|
```
|
|
|
|
You should see a file in your working directory with a `.xpkg` extension. The
|
|
Crossplane CLI will automatically tag and push it to the registry of your
|
|
choosing in the next step if it is the only `.xpkg` in the directory. Otherwise
|
|
you may specify a specific package by using the `-f` flag.
|
|
|
|
```console
|
|
# Set this to the Docker Hub username or OCI registry you wish to use.
|
|
REG=my-package-repo
|
|
kubectl crossplane push configuration ${REG}/getting-started-with-azure:v1.10.1
|
|
```
|
|
|
|
> Note that the Crossplane CLI will not follow symbolic links for files in the
|
|
> root package directory.
|
|
|
|
{{< /tab >}}
|
|
{{< /tabs >}}
|
|
|
|
That's it! You've now built and pushed your package. Take a look at the
|
|
Crossplane [packages] documentation for more information about installing and
|
|
working with packages, or read about other Crossplane [concepts].
|
|
|
|
## Clean Up
|
|
|
|
To clean up, you can simply delete your package directory:
|
|
|
|
```console
|
|
cd ..
|
|
rm -rf crossplane-config
|
|
```
|
|
|
|
<!-- Named Links -->
|
|
|
|
[previous section]: {{<ref "provision-infrastructure" >}}
|
|
[composed]: {{<ref "../concepts/composition" >}}
|
|
[composition]: {{<ref "../concepts/composition" >}}
|
|
[Docker Hub]: https://hub.docker.com/
|
|
[packages]: {{<ref "../concepts/packages" >}}
|
|
[concepts]: {{<ref "../concepts" >}}
|