diff --git a/.gitignore b/.gitignore index b2d6de3..ec80eba 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.idea/ diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 0000000..4d1d64c --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,51 @@ +# BACKLOG.md - Open Control Plane Documentation Project + +## Backlog + + +## Next +- [x] More engaging ansprache (improve language/tone) +- [x] Farbverlauf (gradient styling) [ott] +- [ ] Related? Crossplane usw (add related projects section) [ott] +- [ ] Are CRD reflected? (stretch goal) [ott] +- [ ] Teal farbspektrum und content aware coloring [ott] +- [ ] Maybe in Browser with WASM? (stretch) +- [x] Contributing: Design decisions (ADRs et al) +- [ ] Index: Was möchte ich machen (What do I want to do) +- [ ] Onboarding API - why/what it helps for +- [ ] Johannes change icons +- [ ] End user getting started: MCP Server - max b to publish +- [ ] End user getting started: Edit docs +- [ ] Operators: getting started (currently empty) +- [ ] Operators: Happy Path docs +- [ ] Contributing: Wie mach ich was (How do I do what) - Extend - Fundamental - How to + +## In Progress + +## Review + +## Done + +- [x] Theme-aware section backgrounds (CSS vars instead of hardcoded dark) +- [x] Navbar scroll transparency effect (transparent at top, solid on scroll) +- [x] Footer logo sizing (EU + NeoNephos enlarged) +- [x] Light-mode axolotl drop shadow (0.25 opacity + gradient blob restored) +- [x] Content max-width alignment (1152px hero/features/footer, 1152px navbar) +- [x] Swizzled footer — 3-row layout (EU banner, copyright, legal links) +- [x] NeoNephos SVG logo in footer +- [x] Feature card mouse-tracking glow effect +- [x] Axolotl drop shadow (dark mode) +- [x] Favicon update (axolotl mascot, multi-size .ico) +- [x] SVG graphics/icons (co_axolotl.svg vector trace, 160KB) +- [x] Navbar logo — mirrored axolotl facing left + transparent background variant +- [x] End user getting started: Prerequisites (platform installed) +- [x] End user getting started: authentication/authorization +- [x] End user getting started: pictures (hierarchy diagram) +- [x] Double check no openMCP → OpenControlPlane (cleanup legacy naming) +- [x] SIGs mit rein (include SIGs) +- [x] Community seite - how to participate +- [x] Contributing: Wie kann ich partizipieren (How can I participate) - an der community + +--- + +*This board tracks granular tasks for the Open Control Plane documentation project. For cross-project status, see the main Projects.md overview.* diff --git a/COLOR_SCHEME_REFERENCE.md b/COLOR_SCHEME_REFERENCE.md new file mode 100644 index 0000000..d80fca4 --- /dev/null +++ b/COLOR_SCHEME_REFERENCE.md @@ -0,0 +1,50 @@ +# Role-Based Color Scheme - COMPLETE IMPLEMENTATION + +## CSS Variables (Lines 39-57 in custom.css) + +```css +/* Role-based Teal Spectrum */ +--teal-2: #C2FCEE; +--teal-4: #2CE0BF; +--teal-6: #049F9A; +--teal-7: #07838F; ← End User Primary +--teal-10: #02414C; ← Operator Primary +--teal-11: #012931; ← Contributor Primary + +/* Role Colors */ +--role-enduser-primary: var(--teal-7); #07838F +--role-enduser-secondary: var(--teal-10); #02414C +--role-operator-primary: var(--teal-10); #02414C +--role-operator-secondary: var(--teal-11); #012931 +--role-contributor-primary: transparent; +--role-contributor-border: var(--teal-11); #012931 +``` + +## Where Colors Appear + +### 1. Hero Section Buttons (Landing Page) +- **"Get Started"** → Teal 7 (#07838F), hover: Teal 10 +- **"Run Your Platform"** → Teal 10 (#02414C), hover: Teal 11 +- **"Build Together"** → Transparent, hover: subtle tint + +### 2. Navbar Links (Top Navigation) +- Hover/Active states show role colors +- Bottom border accent appears on hover +- Colors: Teal 7 (users), Teal 10 (operators), Teal 11 (developers) + +### 3. Sidebar (Documentation Sections) +- Left border shows section color +- Active menu items highlighted in role color +- Hover states use lighter tint of role color +- Different color per section: userDocs, operatorDocs, developerDocs + +## Color Gradient Philosophy + +Light → Dark = Beginner → Advanced + +- **Teal 7** (lighter) = End users, getting started, welcoming +- **Teal 10** (medium dark) = Operators, platform management, professional +- **Teal 11** (darkest) = Contributors, developers, technical depth +- **Transparent** = Open invitation to contribute + +This creates a visual progression through the documentation that mirrors the user journey. diff --git a/README.md b/README.md index dbd488d..a77db47 100644 --- a/README.md +++ b/README.md @@ -48,4 +48,4 @@ We as members, contributors, and leaders pledge to make participation in our com ## Licensing -Copyright 2025 SAP SE or an SAP affiliate company and openMCP contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/openmcp-project/docs). +Copyright 2025 SAP SE or an SAP affiliate company and OpenControlPlane contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/openmcp-project/docs). diff --git a/adrs/2025-07-17-local-dns.md b/adrs/2025-07-17-local-dns.md index 7aab839..c8fa2df 100644 --- a/adrs/2025-07-17-local-dns.md +++ b/adrs/2025-07-17-local-dns.md @@ -7,7 +7,7 @@ authors: ## Context and Problem Statement -When creating services on a Kubernetes cluster, they shall be accessible from other clusters within an openMCP landscape. To achieve this a `Gateway` and `HTTPRoute` resource is created. The Gateway controller will assign a routable IP address to the Gateway resource. The HTTPRoute resource will then be used to route traffic to the service. +When creating services on a Kubernetes cluster, they shall be accessible from other clusters within an OpenControlPlane landscape. To achieve this a `Gateway` and `HTTPRoute` resource is created. The Gateway controller will assign a routable IP address to the Gateway resource. The HTTPRoute resource will then be used to route traffic to the service. ```yaml apiVersion: gateway.networking.k8s.io/v1beta1 @@ -49,18 +49,18 @@ spec: port: 80 ``` -The problem is that the service is only reachable via the IP address and not via the hostname. This is because the DNS server in the openMCP landscape does not know about the service and therefore cannot resolve the hostname to the IP address. The Kubernetes dns service only knows how to route to service within the same cluster. On an openMCP landscape however, services must be reachable from other clusters by stable host names. +The problem is that the service is only reachable via the IP address and not via the hostname. This is because the DNS server in the OpenControlPlane landscape does not know about the service and therefore cannot resolve the hostname to the IP address. The Kubernetes dns service only knows how to route to service within the same cluster. On an OpenControlPlane landscape however, services must be reachable from other clusters by stable host names. -Therefore there is a need for a openMCP DNS solution that makes these host names resolvable on all clusters that ar part of the openMCP landscape. +Therefore there is a need for an OpenControlPlane DNS solution that makes these host names resolvable on all clusters that are part of the OpenControlPlane landscape. -## openMCP DNS System Service +## OpenControlPlane DNS System Service -To solve the stated problem, a `openMCP DNS System Service` is needed. This system service will be responsible for the following tasks: +To solve the stated problem, an `OpenControlPlane DNS System Service` is needed. This system service will be responsible for the following tasks: -* Deploy a central openMCP DNS server in the openMCP landscape. This DNS server will be used to resolve all host names in the openMCP base domain `openmcp.cluster`. -* For each cluster in the openMCP landscape, the system service will configure the Kubernetes local DNS service to forward DNS queries for the openMCP base domain to the central openMCP DNS server. This will ensure that all clusters can resolve host names in the openMCP base domain. -* For each Gateway or Ingress resource, the system service will create a DNS entry in the central openMCP DNS server. The DNS entry will map the hostname to the IP address of the Gateway or Ingress resource. -* For each cluster in the openMCP landscape, the system service will annotate the `Cluster` resource with the openMCP base domain. This will help service providers to configure their services to use the openMCP base domain for their host names. +* Deploy a central OpenControlPlane DNS server in the OpenControlPlane landscape. This DNS server will be used to resolve all host names in the OpenControlPlane base domain `openmcp.cluster`. +* For each cluster in the OpenControlPlane landscape, the system service will configure the Kubernetes local DNS service to forward DNS queries for the OpenControlPlane base domain to the central OpenControlPlane DNS server. This will ensure that all clusters can resolve host names in the OpenControlPlane base domain. +* For each Gateway or Ingress resource, the system service will create a DNS entry in the central OpenControlPlane DNS server. The DNS entry will map the hostname to the IP address of the Gateway or Ingress resource. +* For each cluster in the OpenControlPlane landscape, the system service will annotate the `Cluster` resource with the OpenControlPlane base domain. This will help service providers to configure their services to use the OpenControlPlane base domain for their host names. This shall be completely transparent to a service provider. The service provider only needs to create a Gateway or Ingress resource and the DNS entry will be created automatically. @@ -75,7 +75,7 @@ For the example implementation, following components are used: The `DNS Provider` is running on the platform cluster. The `DNS Provider` is deploying an `ETCD` and the cental `CoreDNS` instance on the platform cluster. The `ETCD` instance is used to store the DNS entries. The `CoreDNS` is reading the DNS entries from the `ETCD` instance and is used to resolve the host names. ```yaml -# CoreDNS configuration to read DNS entries from ETCD for the openMCP base domain `openmcp.cluster` +# CoreDNS configuration to read DNS entries from ETCD for the OpenControlPlane base domain `openmcp.cluster` - name: etcd parameters: openmcp.cluster configBlock: |- @@ -95,7 +95,7 @@ containers: - --source=ingress - --source=gateway-httproute - --provider=coredns - - --domain-filter=platform.openmcp.cluster # only detect hostnames in the openMCP base domain belonging to the cluster + - --domain-filter=platform.openmcp.cluster # only detect hostnames in the OpenControlPlane base domain belonging to the cluster env: - name: ETCD_URLS value: http://172.18.200.2:2379 # external routable IP of the ETCD instance running on the platform cluster @@ -123,7 +123,7 @@ subgraph Platform Cluster end ``` -The `DNS Provider` is updating the `CoreDNS` configuration on the platform cluster and on all other clusters. The `CoreDNS` configuration is updated to forward DNS queries for the openMCP base domain to the central `CoreDNS` instance running on the platform cluster. This will ensure that all clusters can resolve host names in the openMCP base domain. +The `DNS Provider` is updating the `CoreDNS` configuration on the platform cluster and on all other clusters. The `CoreDNS` configuration is updated to forward DNS queries for the OpenControlPlane base domain to the central `CoreDNS` instance running on the platform cluster. This will ensure that all clusters can resolve host names in the OpenControlPlane base domain. ```corefile openmcp.cluster { @@ -146,4 +146,4 @@ subgraph Platform Cluster end ``` -Then on any pod in any cluster of the openMCP landscape, the hostname can be resolved to the IP address of the Gateway or Ingress resource. +Then on any pod in any cluster of the OpenControlPlane landscape, the hostname can be resolved to the IP address of the Gateway or Ingress resource. diff --git a/adrs/2025-08-12-mcp-namespace-strategy.md b/adrs/2025-08-12-mcp-namespace-strategy.md index 9e0cfe6..5c1a587 100644 --- a/adrs/2025-08-12-mcp-namespace-strategy.md +++ b/adrs/2025-08-12-mcp-namespace-strategy.md @@ -7,7 +7,7 @@ authors: ## Context and Problem Statement -In the openMCP platform, we need to determine how to organize resources in the Platform Cluster that belong to Managed Control Planes (MCPs). Each MCP represents a separate tenant or customer environment that needs to be isolated and managed independently. The key question is: Should every MCP on the Platform Cluster have its own Namespace to ensure proper isolation, resource management, and security boundaries? +In the OpenControlPlane platform, we need to determine how to organize resources in the Platform Cluster that belong to Managed Control Planes (MCPs). Each MCP represents a separate tenant or customer environment that needs to be isolated and managed independently. The key question is: Should every MCP on the Platform Cluster have its own Namespace to ensure proper isolation, resource management, and security boundaries? Without proper namespace isolation, MCPs could interfere with each other, leading to security vulnerabilities, resource conflicts, and operational complexity. diff --git a/docs/about/concepts/cluster-provider.md b/docs/about/concepts/cluster-provider.md deleted file mode 100644 index 73c72bb..0000000 --- a/docs/about/concepts/cluster-provider.md +++ /dev/null @@ -1,3 +0,0 @@ -# Cluster Providers - -Cluster providers are responsible for the dynamic creation, modification, and deletion of Kubernetes clusters in an openMCP environment. They conceal certain cluster technologies (e.g., [Gardener](https://gardener.cloud/) and [Kubernetes-in-Docker](https://kind.sigs.k8s.io/)) behind a homogeneous interface. This allows operators to install an openMCP system in different environments and on various infrastructure providers without having to adjust the other components of the system accordingly. diff --git a/docs/about/concepts/managed-control-plane.md b/docs/about/concepts/managed-control-plane.md deleted file mode 100644 index e58c9f3..0000000 --- a/docs/about/concepts/managed-control-plane.md +++ /dev/null @@ -1,3 +0,0 @@ -# Managed Control Planes (MCPs) - -Managed Control Planes (MCPs) are at the heart of openMCP. Simply put, they are lightweight Kubernetes clusters that store the desired state and current status of various resources. All resources follow the Kubernetes Resource Model (KRM), allowing infrastructure resources, deployments, etc., to be managed with common Kubernetes tools like kubectl, kustomize, Helm, Flux, ArgoCD, and so on. diff --git a/docs/about/concepts/platform-service.md b/docs/about/concepts/platform-service.md deleted file mode 100644 index aea5020..0000000 --- a/docs/about/concepts/platform-service.md +++ /dev/null @@ -1,3 +0,0 @@ -# Platform Services - -Platform services add functionality to an openMCP environment (not MCPs). Examples include network services (Gateway API, Ingress), audit logs, billing, grouping of MCPs, and system-wide policies. They are installed and configured by the platform operator and apply to the entire system. diff --git a/docs/about/concepts/service-provider.md b/docs/about/concepts/service-provider.md deleted file mode 100644 index 5bfcf14..0000000 --- a/docs/about/concepts/service-provider.md +++ /dev/null @@ -1,3 +0,0 @@ -# Service Providers - -Without service providers, MCPs are of little use. They add functionality such as cloud provider APIs, GitOps, policies, or backup and restore to MCPs. The operators of an openMCP environment decide which service providers are available to end users. The end users can then activate them for their MCPs. diff --git a/docs/about/ecosystem.md b/docs/about/ecosystem.md deleted file mode 100644 index d2b53a6..0000000 --- a/docs/about/ecosystem.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Ecosystem - -openMCP is a platform built on top of amazing open-source projects. The major ones are listed below. - -## Kubernetes - -"[Kubernetes](https://kubernetes.io/), also known as K8s, is an open source system for automating deployment, scaling, and management of containerized applications."[^kubernetes] openMCP not only runs on Kubernetes but also uses the Kubernetes API as the central interface for all human users as well as integrations and automations. The components of openMCP extend the Kubernetes API through [Custom Resource Definitions (CRDs)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/), enabling the use of Kubernetes for configuring more than just compute, storage, and networking resources. - -## Gardener - -[Gardener](https://gardener.cloud/) delivers "fully-managed clusters at scale everywhere with your own Gardener installation".[^gardener] Supported infrastructure includes AWS, Azure, and GCP but also OpenStack, [IronCore](https://github.com/ironcore-dev/gardener-extension-provider-ironcore), [Hetzner Cloud](https://github.com/23technologies/gardener-extension-provider-hcloud), and others. Like openMCP, Gardener is a Kubernetes extension and "adheres to the same principles for resiliency, manageability, observability and high automation by design".[^gardener] openMCP can use Gardener as a [cluster provider](concepts/cluster-provider.md). - -## Open Component Model - -"The [Open Component Model (OCM)](https://ocm.software/) is an open standard that enables teams to describe software artifacts and their lifecycle metadata in a consistent, technology-agnostic way."[^ocm] openMCP uses the OCM to package components and their dependencies, ensuring a reliable delivery to any (even air-gapped) environment. - -## Crossplane - -"[Crossplane](https://www.crossplane.io/) is an open source, CNCF project built on the foundation of Kubernetes to orchestrate anything."[^crossplane] It makes use of providers to connect to various cloud APIs – a concept that is known from Terraform/OpenTofu. Enabling Crossplane as a [service provider](concepts/service-provider.md) in openMCP allows end-users to make use of the rich ecosystem of Crossplane providers. - -## Flux - -"[Flux](https://fluxcd.io/) is a set of continuous and progressive delivery solutions for Kubernetes that are open and extensible."[^fluxcd] When enabled in an openMCP environment, users can benefit from [GitOps](https://www.cncf.io/blog/2025/06/09/gitops-in-2025-from-old-school-updates-to-the-modern-way/) features as part of their [MCPs](concepts/managed-control-plane.md). - -## Kyverno - -"The [Kyverno](https://kyverno.io/) project provides a comprehensive set of tools to manage the complete Policy-as-Code (PaC) lifecycle for Kubernetes and other cloud native environments."[^kyverno] With Kyverno, both team-internal and organization-wide policies can be defined to establish minimum security standards for managed cloud resources or to represent other corporate standards. - -## External Secrets - -"External Secrets Operator is a Kubernetes operator that integrates external secret management systems like AWS Secrets Manager, HashiCorp Vault, [...] and many more. The operator reads information from external APIs and automatically injects the values into a Kubernetes Secret."[^externalsecrets] In conjunction with other services like Crossplane and Flux, users can define their landscapes as templates and deploy them without code duplication. The External Secrets Operator can not only import secrets into an MCP but also push secrets generated in the MCP to other systems. - -## Landscaper - -"Landscaper provides the means to describe, install and maintain cloud-native landscapes. It allows you to express an order of building blocks, connect output with input data and ultimately, bring your landscape to live."[^landscaper] Operators can activate Landscaper as a service provider in their openMCP environment to ease the rollout of more complex software products for their users. - -[^kubernetes]: https://kubernetes.io/ -[^gardener]: https://gardener.cloud/ -[^ocm]: https://ocm.software/docs/overview/about/ -[^crossplane]: https://www.crossplane.io/ -[^fluxcd]: https://fluxcd.io/ -[^kyverno]: https://kyverno.io/ -[^externalsecrets]: https://external-secrets.io/latest/ -[^landscaper]: https://github.com/gardener/landscaper/blob/master/README.md diff --git a/docs/about/project.md b/docs/about/project.md deleted file mode 100644 index 2d5082a..0000000 --- a/docs/about/project.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -slug: / -sidebar_position: 1 ---- - -# About openMCP - -👋 Welcome to the documentation of openMCP. We are part of [ApeiroRA](https://apeirora.eu/content/projects/) which is an Important Project of Common European Interest - Next Generation Cloud Infrastructures and Services (IPCEI-CIS). - -## 🌐 ApeiroRA? - -ApeiroRA is a reference blueprint for an open, flexible, secure, and compliant next-generation cloud-edge continuum and therefore a key contribution to IPCEI-CIS. At a high level, the projects of ApeiroRA allow users to provider-agnostically fetch, request and consume services, and for service providers to describe, offer and provision their services. - -By being open source, ApeiroRA provides a cross-border spillover effect, solidifying the foundation and future of the project. - -Learn more about ApeiroRA by checking out the official website at [https://apeirora.eu/](https://apeirora.eu/). - -## 🤝 openMCP and ApeiroRA - -The Open Managed Control Plane (openMCP) enables extensible Infrastructure- and Configuration-as-Data capabilities as a Service. Based on the Kubernetes Resource Model, all resources in the cloud-edge continuum with ApeiroRA are accessible and managed via a declarative API and corresponding controllers and operators. Together with the controller which understand OCM and declarative deployment orchestrators, consumers can subscribe to a product release-train of software producers and implement an automated, GitOps-driven deployment workflow at the edges. - -## 👥 Get Involved - -We welcome contributions of all kinds, from code to documentation, testing, and design. If you're interested in getting involved, check out our [open issues](https://github.com/issues?q=is%3Aopen+is%3Aissue+org%3Aopenmcp-project+archived%3Afalse+). - -## 🌈 Code of Conduct - -To facilitate a nice environment for all, check out [our Code of Conduct](https://github.com/openmcp-project/.github/blob/main/CODE_OF_CONDUCT.md). - -## 🪙 Funding - -![Bundesministerium für Wirtschaft und Energie (BMWE)-EU funding logo](/img/BMWK-EU.png) diff --git a/docs/community/00-overview.md b/docs/community/00-overview.md new file mode 100644 index 0000000..dbfd1ee --- /dev/null +++ b/docs/community/00-overview.md @@ -0,0 +1,117 @@ +--- +sidebar_position: 1 +--- + +import IconContainer from '@site/src/components/IconContainer'; +import { Users, Calendar, Code2, MessageSquare, Mail, FileText, Globe, Puzzle, CheckCircle, XCircle, Package, Server, Target, Rocket } from 'lucide-react'; + +# Community + +Welcome to the OpenControlPlane community! We're building an open platform for managing cloud infrastructure and services, and we'd love for you to join us. + +## Get Involved + +
+ +
+ + + +

Contribute Code

+

Check our contributing guide and start making your first contribution to the project.

+ Contributing Guide → +
+ +
+ + + +

GitHub Discussions

+

Browse repositories, open issues, and join discussions on GitHub.

+ openmcp-project → +
+ +
+ +## SIG Extensibility + +Special Interest Group focused on making it easy to build, share, and adopt extensions—service providers, cluster providers, and platform services. + +
+ +
+ + + +

SIG Extensibility

+

Make it easy to build, share, and adopt extensions—service providers, cluster providers, and platform services.

+ +
+
Leads: Maximilian Techritz, Christopher Junk (SAP)
+
Meetings: Bi-weekly, Wednesday 3PM CET
+
Mailing List: openMCP-extensibility@lists.neonephos.org
+
+ +
+ View Charter + Subscribe +
+
+ +
+ +### Scope + +**In scope:** +- Developer tooling: templates, frameworks, SDKs +- Increasing service options for end users +- Technical standardization for extensibility + +**Out of scope:** +- Core APIs (ServiceProvider, ClusterProvider, etc.) +- Fundamental platform services (e.g., platform-service-gateway) + +### Subprojects + +**Service Providers:** +- [Crossplane service provider](https://github.com/openmcp-project/service-provider-crossplane) +- [Landscaper service provider](https://github.com/openmcp-project/service-provider-landscaper) +- [Velero service provider](https://github.com/openmcp-project/service-provider-velero) +- [Service provider template](https://github.com/openmcp-project/repository-template) + +**Cluster Providers:** +- [Gardener cluster provider](https://github.com/openmcp-project/cluster-provider-gardener) +- [Kind cluster provider](https://github.com/openmcp-project/cluster-provider-kind) +- Testing infrastructure + +### Starting a New SIG + +Interested in creating a new SIG? See the [SIG template](https://github.com/openmcp-project/community/blob/main/sigs/sig-template.md) and open a [discussion](https://github.com/openmcp-project/community/discussions) in the community repository. + +## Code of Conduct + +We follow the [Contributor Covenant Code of Conduct](https://github.com/openmcp-project/.github/blob/main/CODE_OF_CONDUCT.md) to maintain a welcoming and harassment-free environment for everyone. + +## Related Communities + +
+ +
+ + + +

ApeiroRA

+

European cloud initiative promoting open-source cloud technologies.

+ Visit Website → +
+ +
+ + + +

NeoNephos

+

Cloud-native ecosystem for next-generation infrastructure.

+ Visit Website → +
+ +
diff --git a/docs/developers/00-getting-started.md b/docs/developers/00-getting-started.md deleted file mode 100644 index 3c72e6d..0000000 --- a/docs/developers/00-getting-started.md +++ /dev/null @@ -1,9 +0,0 @@ -# Getting Started - -Here you will find all the information you need to get started with our project. Whether you're a beginner or an experienced developer, this guide will help you set up your environment and start contributing. - -Have a look at our [design documentation](./../about/design/service-provider.md) to understand the architecture and design principles behind the project. - -Check our guide on [how to create a Service Provider](./../developers/service-providers.md) to turn the next Kubernetes-native application into an as-a-Service offering. - -Let's get started in building your first Service Provider for the OpenMCP ecosystem! diff --git a/docs/developers/00-getting-started.mdx b/docs/developers/00-getting-started.mdx new file mode 100644 index 0000000..dc25838 --- /dev/null +++ b/docs/developers/00-getting-started.mdx @@ -0,0 +1,35 @@ +--- +sidebar_position: 0 +--- + +import IconContainer from '@site/src/components/IconContainer'; +import { Users } from 'lucide-react'; + +# Overview + +Welcome to the OpenControlPlane developer community! This is where we build the future of cloud infrastructure management together. + +Whether you're a beginner or an experienced developer, this guide will help you get started with contributing to the OpenControlPlane ecosystem. + +## Getting Started + +Check our guide on [how to create a Service Provider](./serviceprovider/service-providers) to turn the next Kubernetes-native application into an as-a-Service offering. + +## Join the Community + +
+ +
+ + + +

Join SIG Extensibility

+

Connect with developers building providers and extensions for OpenControlPlane.

+ Community Hub → +
+ +
+ +--- + +Let's get started in building your first provider for the OpenControlPlane ecosystem! diff --git a/docs/developers/adrs/_category_.json b/docs/developers/adrs/_category_.json new file mode 100644 index 0000000..f864278 --- /dev/null +++ b/docs/developers/adrs/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "ADRs", + "position": 99 +} diff --git a/docs/developers/provider_deployment.md b/docs/developers/clusterprovider/01-deployment.mdx similarity index 88% rename from docs/developers/provider_deployment.md rename to docs/developers/clusterprovider/01-deployment.mdx index ff3ba07..2705462 100644 --- a/docs/developers/provider_deployment.md +++ b/docs/developers/clusterprovider/01-deployment.mdx @@ -1,15 +1,17 @@ -# Provider Deployment +--- +sidebar_position: 1 +--- -The openMCP architecture knows three different kinds of providers: -- `ClusterProviders` manage kubernetes clusters and access to them -- `PlatformServices` provide landscape-wide service functionalities -- `ServiceProviders` provide the actual services that can be consumed by customers via the ManagedControlPlanes +# Deploy -All providers can automatically be deployed via the corresponding provider resources: `ClusterProvider`, `PlatformService`, and `ServiceProvider`. The [openmcp-operator](https://github.com/openmcp-project/openmcp-operator) is responsible for these resources. +`ClusterProviders` manage Kubernetes clusters and access to them within the OpenControlPlane ecosystem. + +They can automatically be deployed via the `ClusterProvider` resource. The [openmcp-operator](https://github.com/openmcp-project/openmcp-operator) is responsible for these resources. + +All providers are cluster-scoped resources. + +## Example ClusterProvider Resource -For now, the spec of all three provider kinds looks exactly the same, which is why they are all explained together. -All of them are cluster-scoped resources. -This is a `ClusterProvider` resource as an example: ```yaml apiVersion: openmcp.cloud/v1alpha1 kind: ClusterProvider @@ -22,12 +24,10 @@ spec: ## Common Provider Contract -This section explains the contract that provider implementations must follow for the deployment to work. +All provider types (ClusterProviders, ServiceProviders, PlatformServices) follow the same deployment contract. ### Executing the Binary -Further information on how the provider binary is executed can be found below. - #### Image Each provider implementation must provide a container image with the provider binary set as an entrypoint. @@ -83,9 +83,12 @@ Providers generally live in the platform cluster, so they can simply access it b This flow is already implemented in the library function [`CreateAndWaitForCluster](https://github.com/openmcp-project/openmcp-operator/blob/v0.11.2/lib/clusteraccess/clusteraccess.go#L387). -### Examples +## Deployment Example + +The `ClusterProvider` resource above will result in the following `Job` and `Deployment` (redacted to the more relevant fields): + +### Init Job -Basically, the `ClusterProvider` from the example above will result in the following `Job` and `Deployment` (redacted to the more relevant fields): ```yaml apiVersion: batch/v1 kind: Job @@ -161,6 +164,9 @@ spec: serviceAccount: gardener-init serviceAccountName: gardener-init ``` + +### Controller Deployment + ```yaml apiVersion: apps/v1 kind: Deployment diff --git a/docs/developers/clusterproviders.md b/docs/developers/clusterprovider/02-develop.mdx similarity index 90% rename from docs/developers/clusterproviders.md rename to docs/developers/clusterprovider/02-develop.mdx index a7104c9..fe27cc0 100644 --- a/docs/developers/clusterproviders.md +++ b/docs/developers/clusterprovider/02-develop.mdx @@ -1,12 +1,19 @@ -# Cluster Providers +--- +sidebar_position: 2 +--- -A *ClusterProvider* is one of the three provider types in the openMCP architecture (the other two being *PlatformService* and *ServiceProvider*). ClusterProviders are responsible for managing kubernetes clusters and access to them, based on our [cluster API](https://github.com/openmcp-project/openmcp-operator/tree/main/api/clusters/v1alpha1). +# Develop -This document aims to describe the tasks of a ClusterProvider and the contract that it needs to fulfill in order to work within the openMCP ecosystem. +:::info Coming Soon +A comprehensive guide for developing Cluster Providers from scratch is coming soon. +::: -## Deploying a ClusterProvider +Cluster Providers manage Kubernetes clusters and provide access to them within the OpenControlPlane ecosystem. They follow the same general patterns as Service Providers. -ClusterProviders are usually deployed via the [provider deployment](./provider_deployment.md) mechanism and need to stick to the corresponding contract. +For now, you can: +- Review existing implementations: [cluster-provider-gardener](https://github.com/openmcp-project/cluster-provider-gardener) and [cluster-provider-kind](https://github.com/openmcp-project/cluster-provider-kind) +- Refer to the [Deploy Guide](./01-deployment.mdx) for the common deployment contract +- Check the [Service Provider Develop Guide](../serviceprovider/02-service-providers.mdx) for general patterns that apply to all provider types ## Implementing a ClusterProvider @@ -44,7 +51,7 @@ spec: version: 1.32.2 ``` -`spec.providerRef` is the name of the ClusterProvider that created this `ClusterProfile`. It should be filled with the value that the provider received via its [`--provider-name`](./provider_deployment.md#arguments) argument. +`spec.providerRef` is the name of the ClusterProvider that created this `ClusterProfile`. It should be filled with the value that the provider received via its `--provider-name` argument. `spec.providerConfigRef` is the name of the provider configuration that is responsible for this profile. Whether this refers to an actual k8s resource, an internal value or just a static string depends on the provider implementation. It is used as a label value though and therefore has to match the corresponding regex. @@ -99,7 +106,7 @@ The rest of the reconciliation logic is pretty much provider specific: If the `C #### Status Reporting -Since creating, updating, or deleting k8s clusters can easily take several minutes, reporting the current status is very important here. It is recommended to make good use of the conditions that are part of the status. ClusterProviders must adhere to the [general status reporting rules](./general.md#status-reporting). +Since creating, updating, or deleting k8s clusters can easily take several minutes, reporting the current status is very important here. It is recommended to make good use of the conditions that are part of the status. ClusterProviders must adhere to general status reporting rules. In addition to the common status, the `Cluster` status contains a few more fields that can be set by the ClusterProvider: - `apiServer` should be filled with the k8s cluster's apiserver endpoint, as soon as it is known. @@ -209,7 +216,7 @@ It modifies the `AccessRequest` in the following way: This means that the AccessRequest controller in a ClusterProvider must only act on AccessRequests that have both of the aforementioned labels set. They can then expect `spec.clusterRef` to be set and don't need to check for `spec.requestRef`. -It is recommended to use [event filtering](./general.md#event-filtering) to avoid reconciling AccessRequests that belong to another provider or have not yet been prepared by the generic controller. The controller-utils library contains a `HasLabelPredicate` filter that can be used for both, verifying existence of a label as well as checking if it has a specific value: +It is recommended to use event filtering to avoid reconciling AccessRequests that belong to another provider or have not yet been prepared by the generic controller. The controller-utils library contains a `HasLabelPredicate` filter that can be used for both, verifying existence of a label as well as checking if it has a specific value: ```go import ( ctrl "sigs.k8s.io/controller-runtime" diff --git a/docs/developers/clusterprovider/03-examples.mdx b/docs/developers/clusterprovider/03-examples.mdx new file mode 100644 index 0000000..6893756 --- /dev/null +++ b/docs/developers/clusterprovider/03-examples.mdx @@ -0,0 +1,43 @@ +--- +sidebar_position: 3 +--- + +# Examples + +Cluster Providers that manage Kubernetes clusters within OpenControlPlane. Use these as inspiration to build your own integrations or contribute improvements. + +## Official Cluster Providers + +
+ +
+
+ Gardener Cluster Provider +

cluster-provider-gardener

+
+
+ Integrates Gardener for managing production-grade Kubernetes clusters across multiple cloud providers. +
+
+ GitHub +
+
+ +
+
+ Kind Cluster Provider +

cluster-provider-kind

+
+
+ Integrates Kind (Kubernetes in Docker) for local development and testing environments. +
+
+ GitHub +
+
+ +
+ +These projects serve as reference implementations for building Cluster Providers. Check out the [Deploy Guide](./01-deployment.mdx) to create your own, or contribute improvements to existing providers. + +**Want to get involved?** Join our developer community through [SIG Extensibility](https://github.com/openmcp-project/community/tree/main/sig-extensibility) - see the [Build Together overview](../getting-started#join-the-community) for details on how to connect with other provider developers. diff --git a/docs/developers/clusterprovider/04-design.mdx b/docs/developers/clusterprovider/04-design.mdx new file mode 100644 index 0000000..daa7601 --- /dev/null +++ b/docs/developers/clusterprovider/04-design.mdx @@ -0,0 +1,13 @@ +--- +sidebar_position: 4 +--- + +# Design + +:::info Coming Soon +Detailed design documentation for Cluster Providers is coming soon. +::: + +Cluster Providers are responsible for managing Kubernetes clusters and providing access to them within the OpenControlPlane ecosystem. They follow similar architectural patterns to Service Providers. + +For reference, you can review the [Service Provider Design Documentation](../serviceprovider/design.md) which outlines concepts that apply to all provider types in the OpenControlPlane ecosystem. diff --git a/docs/developers/clusterprovider/_category_.json b/docs/developers/clusterprovider/_category_.json new file mode 100644 index 0000000..193856b --- /dev/null +++ b/docs/developers/clusterprovider/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Cluster Providers", + "position": 3 +} diff --git a/docs/developers/general.md b/docs/developers/general.mdx similarity index 97% rename from docs/developers/general.md rename to docs/developers/general.mdx index 8a018ce..916a6cb 100644 --- a/docs/developers/general.md +++ b/docs/developers/general.mdx @@ -1,6 +1,10 @@ +--- +sidebar_position: 2 +--- + # General Controller Guidelines -This document contains some general guidelines for contributing code to openMCP controllers. The goal is to align the coding and make all controllers look and behave similarly. +This document contains some general guidelines for contributing code to OpenControlPlane controllers. The goal is to align the coding and make all controllers look and behave similarly. ## Reconcile Logic diff --git a/docs/developers/platformprovider/01-deployment.mdx b/docs/developers/platformprovider/01-deployment.mdx new file mode 100644 index 0000000..6567a13 --- /dev/null +++ b/docs/developers/platformprovider/01-deployment.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 1 +--- + +# Deploy + +:::info Coming Soon +Documentation for deploying Platform Providers is coming soon. Platform Providers follow the same deployment contract as Service Providers and Cluster Providers. +::: + +For now, please refer to the [Service Provider Deploy Guide](../serviceprovider/01-deployment.mdx) for the common deployment contract that applies to all provider types. diff --git a/docs/developers/platformprovider/03-develop.mdx b/docs/developers/platformprovider/03-develop.mdx new file mode 100644 index 0000000..7ca4722 --- /dev/null +++ b/docs/developers/platformprovider/03-develop.mdx @@ -0,0 +1,15 @@ +--- +sidebar_position: 3 +--- + +# Develop + +:::info Coming Soon +A comprehensive guide for developing Platform Providers from scratch is coming soon. +::: + +Platform Providers deliver complete platform capabilities and services within the OpenControlPlane ecosystem. They follow the same general patterns as Service Providers and Cluster Providers. + +For now, you can: +- Refer to the [Deploy Guide](./01-deployment.mdx) for the common deployment contract +- Check the [Service Provider Develop Guide](../serviceprovider/02-service-providers.mdx) for general patterns that apply to all provider types diff --git a/docs/developers/platformprovider/04-examples.mdx b/docs/developers/platformprovider/04-examples.mdx new file mode 100644 index 0000000..a1397ad --- /dev/null +++ b/docs/developers/platformprovider/04-examples.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 4 +--- + +# Examples + +:::info Coming Soon +Platform Provider examples and templates are coming soon. +::: + +Platform Providers deliver complete platform capabilities within OpenControlPlane. Stay tuned for official examples and reference implementations. diff --git a/docs/developers/platformprovider/05-design.mdx b/docs/developers/platformprovider/05-design.mdx new file mode 100644 index 0000000..9c8d299 --- /dev/null +++ b/docs/developers/platformprovider/05-design.mdx @@ -0,0 +1,13 @@ +--- +sidebar_position: 5 +--- + +# Design + +:::info Coming Soon +Detailed design documentation for Platform Providers is coming soon. +::: + +Platform Providers deliver complete platform capabilities within the OpenControlPlane ecosystem. They follow similar architectural patterns to Service Providers and Cluster Providers. + +For reference, you can review the [Service Provider Design Documentation](../serviceprovider/design.md) which outlines concepts that apply to all provider types in the OpenControlPlane ecosystem. diff --git a/docs/developers/platformprovider/_category_.json b/docs/developers/platformprovider/_category_.json new file mode 100644 index 0000000..f66f842 --- /dev/null +++ b/docs/developers/platformprovider/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Platform Providers", + "position": 4 +} diff --git a/docs/developers/serviceprovider/01-deployment.mdx b/docs/developers/serviceprovider/01-deployment.mdx new file mode 100644 index 0000000..0482462 --- /dev/null +++ b/docs/developers/serviceprovider/01-deployment.mdx @@ -0,0 +1,90 @@ +--- +sidebar_position: 1 +--- + +# Deploy + +`ServiceProviders` provide the actual services that can be consumed by customers via the ManagedControlPlanes within the OpenControlPlane ecosystem. + +They can automatically be deployed via the `ServiceProvider` resource. The [openmcp-operator](https://github.com/openmcp-project/openmcp-operator) is responsible for these resources. + +All providers are cluster-scoped resources. + +## Example ServiceProvider Resource + +```yaml +apiVersion: openmcp.cloud/v1alpha1 +kind: ServiceProvider +metadata: + name: example-service +spec: + image: ghcr.io/openmcp-project/images/service-provider-example:v1.0.0 + verbosity: INFO +``` + +## Common Provider Contract + +All provider types (ClusterProviders, ServiceProviders, PlatformServices) follow the same deployment contract. + +### Executing the Binary + +#### Image + +Each provider implementation must provide a container image with the provider binary set as an entrypoint. + +#### Subcommands + +The provider binary must take two subcommands: +- `init` initializes the provider. This usually means deploying CRDs for custom resources used by the controller(s). + - The `init` subcommand is executed as a job once whenever the deployed version of the provider changes. +- `run` runs the actual controller(s) required for the provider. + - The `run` subcommand is executed in a pod as part of a deployment. + - The pods with the `run` command are only started after the init job has successfully run through. + - It may be run multiple times in parallel (high-availability), so the provider implementation should support this, e.g. via leader election. + +#### Arguments + +Both subcommands take the same arguments, which are explained below. These arguments will always be passed into the provider. +- `--environment` *any lowercase string* + - The *environment* argument is meant to distinguish between multiple environments (=platform clusters) watching the same onboarding cluster. For example, there could be a public environment and another fenced one - both watch the same resources on the same cluster, but only one of them is meant to react on each resource, depending on its configuration. + - Most setups will probably use only a single environment. + - Will likely be set to the landscape name (e.g. `canary`, `live`) most of the time. +- `--provider-name` *any lowercase string* + - This argument contains the name of the k8s provider resource from which this pod was created. + - If ever multiple instances of the same provider are deployed in the same landscape, this value can be used to differentiate between them. +- `--verbosity` or `-v` *enum: ERROR, INFO, or DEBUG* + - This value specifies the desired logging verbosity for the provider. + +#### Environment Variables + +The following environment variables can be expected to be set: +- `POD_NAME` + - Name of the pod the provider binary runs in. +- `POD_NAMESPACE` + - Namespace of the pod the provider binary runs in. +- `POD_IP` + - IP address of the pod the provider binary runs in. +- `POD_SERVICE_ACCOUNT_NAME` + - Name of the service account that is used to run the provider. + +#### Customizations + +While it is possible to customize some aspects of how the provider binary is executed, such as adding additional environment variables, overwriting the subcommands, adding additional arguments, etc., this should only be done in exceptional cases to keep the complexity of setting up an openMCP landscape as low as possible. + +### Configuration + +Passing configuration into the provider binary via a command-line argument is not desired. If the provider requires configuration of some kind, it is expected to read it from one or more k8s resources, potentially even running a controller to reconcile these resources. The `init` subcommand can be used to register the CRDs for the configuration resources, although this leads to the disadvantage of the configuration resource only been known after the provider has already been started, which can cause problems with gitOps (or similar deployment methods that deploy all resources at the same time). + +### Tips and Tricks + +#### Getting Access to the Onboarding Cluster + +Providers generally live in the platform cluster, so they can simply access it by using the in-cluster configuration. Getting access to the onboarding cluster is a little bit more tricky: First, the `Cluster` resource of the onboarding cluster itself or any `ClusterRequest` pointing to it is required. The provider can simply create its own `ClusterRequest` with purpose `onboarding` - a little trick that is possible due to the shared nature of the onboarding cluster, all requests to it will result in a reference to the same `Cluster`. Then, the provider needs to create an `AccessRequest` with the desired permissions and wait until it is ready. This will result in a secret containing a kubeconfig for the onboarding cluster. + +This flow is already implemented in the library function [`CreateAndWaitForCluster](https://github.com/openmcp-project/openmcp-operator/blob/v0.11.2/lib/clusteraccess/clusteraccess.go#L387). + +## Deployment Example + +The `ServiceProvider` resource above will result in similar `Job` and `Deployment` resources as ClusterProviders, with the main difference being the `kind: ServiceProvider` annotation and corresponding labels. + +The deployment structure follows the same pattern with an init job for CRD installation and a controller deployment for the actual service provider logic. diff --git a/docs/developers/service-providers.md b/docs/developers/serviceprovider/02-service-providers.mdx similarity index 97% rename from docs/developers/service-providers.md rename to docs/developers/serviceprovider/02-service-providers.mdx index 5709841..b88923a 100644 --- a/docs/developers/service-providers.md +++ b/docs/developers/serviceprovider/02-service-providers.mdx @@ -1,6 +1,6 @@ -# Service Providers +# Develop -This guide shows you how to create Service Provider for the OpenMCP ecosystem from scratch. Service Providers are the heart of the OpenMCP platform, as they provide the capabilities to offer Infrastructure as Data services to end users. +This guide shows you how to create Service Provider for the OpenControlPlane ecosystem from scratch. Service Providers are the heart of the OpenControlPlane platform, as they provide the capabilities to offer Infrastructure as Data services to end users. In this guide, we will walk you through the steps of creating a Service Provider using the [service-provider-template](https://github.com/openmcp-project/service-provider-template), explain the context a service provider operates in, and demonstrate how to run end-to-end tests for it. @@ -15,7 +15,7 @@ A service provider consists of the following two major parts, similar to a regul - **A user-facing ServiceProviderAPI**: This allows end users to request a `DomainService` for a `ManagedControlPlane`, e.g. `FooService` or `Velero`. - **A controller that reconciles the ServiceProviderAPI**: This controller manages the lifecycle of the provided `DomainService` and its API (such as `Foo` or the CRDs of Velero). -For a visual overview of how these components fit into an openMCP installation, refer to the [service provider deployment model](https://openmcp-project.github.io/docs/about/design/service-provider#deployment-model). +For a visual overview of how these components fit into an OpenControlPlane installation, refer to the [service provider deployment model](https://openmcp-project.github.io/docs/about/design/service-provider#deployment-model). ## Prerequisites @@ -26,7 +26,7 @@ Finally, ensure that you have Go installed. You can download it from [go.dev](ht ## Service Provider Template Usage -The template allows you to create a service provider without requiring deep knowledge of the underlying OpenMCP platform. +The template allows you to create a service provider without requiring deep knowledge of the underlying OpenControlPlane platform. Run the following command to generate a new provider. Replace `velero` with the kind of your service: diff --git a/docs/developers/serviceprovider/03-examples.mdx b/docs/developers/serviceprovider/03-examples.mdx new file mode 100644 index 0000000..a7a29e4 --- /dev/null +++ b/docs/developers/serviceprovider/03-examples.mdx @@ -0,0 +1,114 @@ +--- +sidebar_position: 3 +--- + +# Examples + +Community Service Providers that extend OpenControlPlane. Use these as inspiration to build your own integrations or contribute improvements. + +## Official Service Providers + +
+ +
+
+ +

service-provider-velero

+
+
+ Integrates Velero for backup and disaster recovery capabilities in OpenControlPlane. +
+
+ GitHub +
+
+ +
+
+ Crossplane Service Provider +

service-provider-crossplane

+
+
+ Integrates Crossplane into OpenControlPlane for managing cloud infrastructure resources. +
+
+ GitHub +
+
+ +
+
+ Landscaper Service Provider +

service-provider-landscaper

+
+
+ Brings Gardener Landscaper to OpenControlPlane for managing complex cloud-native landscapes. +
+
+ GitHub +
+
+ +
+
+ Kyverno Service Provider +

service-provider-kyverno

+
+
+ Integrates Kyverno for policy management and governance in OpenControlPlane. +
+
+ GitHub +
+
+ +
+
+ Flux Service Provider +

service-provider-flux

+
+
+ Integrates Flux for GitOps continuous delivery in OpenControlPlane. +
+
+ GitHub +
+
+ +
+
+ External Secrets Service Provider +

service-provider-external-secrets

+
+
+ Integrates External Secrets Operator for secure secret management in OpenControlPlane. +
+
+ GitHub +
+
+ +
+ +## Community Service Providers + +
+ +
+
+ +

Build Your Own

+
+
+ Use our template to create your own Service Provider and contribute it to the community. +
+
+ Template Repository +
+
+ +
+ +These projects serve as reference implementations for building Service Providers. Check out the [Deploy Guide](./01-deployment.mdx) to create your own, or contribute improvements to existing providers. + +**Want to get involved?** Join our developer community through [SIG Extensibility](https://github.com/openmcp-project/community/tree/main/sig-extensibility) - see the [Build Together overview](../getting-started#join-the-community) for details on how to connect with other provider developers. diff --git a/docs/developers/serviceprovider/_category_.json b/docs/developers/serviceprovider/_category_.json new file mode 100644 index 0000000..2d93286 --- /dev/null +++ b/docs/developers/serviceprovider/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Service Providers", + "position": 2 +} diff --git a/docs/about/design/service-provider.md b/docs/developers/serviceprovider/design.md similarity index 92% rename from docs/about/design/service-provider.md rename to docs/developers/serviceprovider/design.md index 03109fc..a0a886e 100644 --- a/docs/about/design/service-provider.md +++ b/docs/developers/serviceprovider/design.md @@ -1,10 +1,10 @@ -# Service Providers +# Design -This document outlines the `ServiceProvider` domain and its technical considerations within the context of the [openMCP project](https://github.com/openmcp-project/), providing a foundation for understanding its architecture and operational aspects. +This document outlines the `ServiceProvider` domain and its technical considerations within the context of the [OpenControlPlane project](https://github.com/openmcp-project/), providing a foundation for understanding its architecture and operational aspects. ## Goals -- Define clear terminology around `ServiceProvider` within the openMCP project +- Define clear terminology around `ServiceProvider` within the OpenControlPlane project - Establish the scope of a `ServiceProvider`, including its responsibilities and boundaries - Define a `ServiceProvider` implementation layer to implement common features and ensure consistency across `ServiceProvider` instances - Outline how a `ServiceProvider` can be validated @@ -16,14 +16,14 @@ This document outlines the `ServiceProvider` domain and its technical considerat ## Terminology -- `End Users`: These are the consumers of services provided by an openMCP platform installation. They operate on the `OnboardingCluster` and `MCPCluster` (see [deployment model](#deployment-model)). -- `Platform Operators`: These are either human users or technical systems that are responsible for managing an openMCP platform installation. While they may operate on any cluster, their primary focus is on the `PlatformCluster` and `WorkloadCluster`. +- `End Users`: These are the consumers of services provided by an OpenControlPlane platform installation. They operate on the `OnboardingCluster` and `MCPCluster` (see [deployment model](#deployment-model)). +- `Platform Operators`: These are either human users or technical systems that are responsible for managing an OpenControlPlane platform installation. While they may operate on any cluster, their primary focus is on the `PlatformCluster` and `WorkloadCluster`. ## Domain A `ServiceProvider` enables platform operators to offer managed `DomainServices` to end users. A `DomainService` is a third-party service that delivers its functionality to end users through a `DomainServiceAPI`. -For example, consider an openMCP installation that aims to provide [Crossplane](https://www.crossplane.io/) as a managed service to its end users. Let's assume that end users specifically want to use the `Object` API of [provider-kubernetes](https://github.com/crossplane-contrib/provider-kubernetes), to create Kubernetes objects on their own Kubernetes clusters without the need to manage Crossplane themselves. +For example, consider an OpenControlPlane installation that aims to provide [Crossplane](https://www.crossplane.io/) as a managed service to its end users. Let's assume that end users specifically want to use the `Object` API of [provider-kubernetes](https://github.com/crossplane-contrib/provider-kubernetes), to create Kubernetes objects on their own Kubernetes clusters without the need to manage Crossplane themselves. If we map this to the terminology of a `DomainService` and `DomainServiceAPI`: diff --git a/docs/operators/00-getting-started.md b/docs/operators/00-getting-started.md deleted file mode 100644 index bad5562..0000000 --- a/docs/operators/00-getting-started.md +++ /dev/null @@ -1 +0,0 @@ -# Getting Started diff --git a/docs/operators/00-overview.md b/docs/operators/00-overview.md new file mode 100644 index 0000000..62f1f1c --- /dev/null +++ b/docs/operators/00-overview.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 0 +--- + +import Tabs from '@theme/Tabs'; +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; + +# Overview + +To set up and and manage OpenControlPlane landscapes, a concept named bootstrapping is used. +Bootstrapping works for creating new landscapes as well as updating existing landscapes with new versions of OpenControlPlane. +The bootstrapping involves the creation of a GitOps process where the desired state of the landscape is stored in a Git repository and is being synced to the actual landscape using FluxCD. +The operator of a landscape can configure the bootstrapping to their liking by providing a bootstrapping configuration that controls the configuration of the openmcp-operator including all desired cluster-providers, service-providers, and platform services. +The bootstrapping is performed by the `openmcp-bootstrapper` command line tool (https://github.com/openmcp-project/bootstrapper). + +## Provider Types + +OpenControlPlane uses three types of providers to deliver infrastructure and services: + +![Provider Types](/img/provider_types.png) + + +- **ServiceProvider** — Adds functionality to Managed Control Planes, such as hyperscaler providers, cloud provider APIs, GitOps, policies, or backup and many more allowing your teams to make their unique cloud landscapes to be replicatable and robust through the power of control planes.. +- **PlatformService** — Extends the entire OpenControlPlane environment with system-wide features like network services, audit logs, billing, and policies. Anything you want to make available for all control planes and stakeholders on the platform. +- **ClusterProvider** — Manages the dynamic creation, modification, and deletion of Kubernetes clusters under the hood. This allows our platform to run in any region or datacenter. + +## General Bootstrapping Architecture + +```mermaid +flowchart TD + subgraph OCI Registry + A[openMCP Root OCM Component] + B[openmcp-operator] --> A + C[Cluster Provider] --> A + D[Service Provider] --> A + E[Platform Service] --> A + F[GitOps Templates] --> A + end + + subgraph GitRepo[Git Repository] + G[Kustomization] + end + + subgraph Target Kubernetes Cluster + H[GitSource] + I[Kustomization] + I --> G + end + + subgraph openmcp-bootstrapper + J[Bootstrapper CLI] + J --> A + J --> G + J --> H + J --> I + end + + H --> GitRepo +``` + +The `openMCP Root OCM Component` (github.com/openmcp-project/openmcp) contains references to the `openmcp-operator`, the `gitops-templates` (github.com/openmcp-project/gitops-templates) as well as a list of cluster providers, service providers and platform services that can be deployed. +The `openMCP Root OCM Component` acts as the source of the available versions, image locations and deployment configuration of an openMCP landscape. + +The `Git Repository` contains the desired state of the openMCP landscape. The desired state is encoded in a set of Kubernetes manifests that are organized and templated using Kustomize. The `Git Repository` is being updated by the `openmcp-bootstrapper` CLI tool for the information provided in the `openMCP Root OCM Component` as well as the bootstrapping configuration provided by the operator. + +The `openmcp-bootstrapper` reads the `openMCP Root OCM Component` from an OCI registry to retrieve the `GitOps Templates` as well as the image locations of the FluxCD tool, the `openmcp-operator`, the cluster providers, the service providers and the platform services. The templated `GitOpsTemplate` is applied to the `Git Repository` and the templated FluxCD deployment is applied to the `Target Kubernetes Cluster`. The `openmcp-bootstrapper` also creates a FluxCD `GitSource` based on the provided Git repository URL and credentials. +The `openmcp-bootstrapper` then creates a FluxCD `Kustomizations` that points to the `Git Repository` and applies it to the `Target Kubernetes Cluster`. diff --git a/docs/operators/01-boostrapping.md b/docs/operators/01-boostrapping.md deleted file mode 100644 index 00be3cd..0000000 --- a/docs/operators/01-boostrapping.md +++ /dev/null @@ -1,2122 +0,0 @@ -import Tabs from '@theme/Tabs'; -import CodeBlock from '@theme/CodeBlock'; -import TabItem from '@theme/TabItem'; - -# openMCP Landscape Bootstrapping - -To set up and and manage openMCP landscapes, a concept named bootstrapping is used. -Bootstrapping works for creating new landscapes as well as updating existing landscapes with new versions of openMCP. -The bootstrapping involves the creation of a GitOps process where the desired state of the landscape is stored in a Git repository and is being synced to the actual landscape using FluxCD. -The operator of a landscape can configure the bootstrapping to their liking by providing a bootstrapping configuration that controls the configuration of the openmcp-operator including all desired cluster-providers, service-providers, and platform services. -The bootstrapping is performed by the `openmcp-bootstrapper` command line tool (https://github.com/openmcp-project/bootstrapper). - -## General Bootstrapping Architecture - -```mermaid -flowchart TD - subgraph OCI Registry - A[openMCP Root OCM Component] - B[openmcp-operator] --> A - C[Cluster Provider] --> A - D[Service Provider] --> A - E[Platform Service] --> A - F[GitOps Templates] --> A - end - - subgraph GitRepo[Git Repository] - G[Kustomization] - end - - subgraph Target Kubernetes Cluster - H[GitSource] - I[Kustomization] - I --> G - end - - subgraph openmcp-bootstrapper - J[Bootstrapper CLI] - J --> A - J --> G - J --> H - J --> I - end - - H --> GitRepo -``` - -The `openMCP Root OCM Component` (github.com/openmcp-project/openmcp) contains references to the `openmcp-operator`, the `gitops-templates` (github.com/openmcp-project/gitops-templates) as well as a list of cluster providers, service providers and platform services that can be deployed. -The `openMCP Root OCM Component` acts as the source of the available versions, image locations and deployment configuration of an openMCP landscape. - -The `Git Repository` contains the desired state of the openMCP landscape. The desired state is encoded in a set of Kubernetes manifests that are organized and templated using Kustomize. The `Git Repository` is being updated by the `openmcp-bootstrapper` CLI tool for the information provided in the `openMCP Root OCM Component` as well as the bootstrapping configuration provided by the operator. - -The `openmcp-bootstrapper` reads the `openMCP Root OCM Component` from an OCI registry to retrieve the `GitOps Templates` as well as the image locations of the FluxCD tool, the `openmcp-operator`, the cluster providers, the service providers and the platform services. The templated `GitOpsTemplate` is applied to the `Git Repository` and the templated FluxCD deployment is applied to the `Target Kubernetes Cluster`. The `openmcp-bootstrapper` also creates a FluxCD `GitSource` based on the provided Git repository URL and credentials. -The `openmcp-bootstrapper` then creates a FluxCD `Kustomizations` that points to the `Git Repository` and applies it to the `Target Kubernetes Cluster`. - -### Prerequisites - -* A target Kubernetes cluster that matches the desired cluster provider being used (e.g. `Kind` for local testing, `Gardener` for Gardener Shoots) -* A Git repository that will be used to store the desired state of the openMCP landscape -* An OCI registry that contains the `openMCP Root OCM Component` (e.g. `ghcr.io/openmcp-project`) - -:::info -The Git repository used in the following examples must exist before running the `openmcp-bootstrapper` CLI tool. The `openmcp-bootstrapper` is using the default branch (like `main`) as a source to create the desired branch. -The default branch may not be empty, but it should not contain any files or folders that would conflict with the files and folders created by the `openmcp-bootstrapper`. A recommendation is to create an empty repository with a `README.md` file. -::: - -#### Download the `openmcp-bootstrapper` CLI tool - -The `openmcp-bootstrapper` CLI tool can be downloaded as an OCI image from an OCI registry (e.g. `ghcr.io/openmcp-project`). -In this example docker will be used to run the `openmcp-bootstrapper` CLI tool. If you don't use docker, adjust the command accordingly. - -Retrieve the latest version of the `openmcp-bootstrapper`: - -```shell -TAG=$(curl -s "https://api.github.com/repos/openmcp-project/bootstrapper/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) -export OPENMCP_BOOTSTRAPPER_VERSION="${TAG}" -``` - -Pull the latest version of the `openmcp-bootstrapper`: - -```shell -docker pull ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} -``` - -## Example using the Kind Cluster Provider - -### Requirements - -* [Docker](https://docs.docker.com/get-docker/) installed and running. Docker alternatively can be replaced with another OCI runtime (e.g. Podman) that can run the `openmcp-bootstrapper` CLI tool as an OCI image. -* [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) installed - -:::info -If you are using a docker alternative, make sure that it is correctly setup regarding Docker compatibility. In case of Podman, you should find a corresponding configuration under `Settings` in the Podman UI. -::: - -### Create a configuration folder - -Create a directory that will be used to store the configuration files and the kubeconfig files. -To keep this example simple, we will use a single directory named `config` in the current working directory. - -```shell -mkdir config -``` - -All following examples will use the `config` directory as the configuration directory. If you use a different directory, replace all occurrences of `config` with your desired directory path. - -Create a directory named `kubeconfigs` in the configuration folder to store the kubeconfig files of the created clusters. - -```shell -mkdir kubeconfigs -``` - -### Create the Kind configuration file (kind-config.yaml) in the configuration folder - -```yaml -apiVersion: kind.x-k8s.io/v1alpha4 -kind: Cluster -nodes: -- role: control-plane - extraMounts: - - hostPath: /var/run/docker.sock - containerPath: /var/run/host-docker.sock -``` - -### Create the Kind cluster - -Create the Kind cluster using the configuration file created in the previous step. - -:::warning - -Please check if your current `kind` network has a `/16` subnet. This is required for our cluster-provider-kind. -You can check the current network configuration using: - -```shell -docker network inspect kind | jq ".[].IPAM.Config.[].Subnet" -"172.19.0.0/16" -``` - -If the result is not specifying `/16` but something smaller like `/24` you need to delete the network and create a new one. For that **all kind clusters needs to be deleted**. Then run: - -```shell -docker network rm kind - -docker network create kind --subnet 172.19.0.0/16 -``` - -::: - -:::info Podman Support -In case you are using Podman instead of Docker, it is currently required to first create a suitable network for the Kind cluster by executing the following command before creating the Kind cluster itself. - -```shell -podman network create kind --subnet 172.19.0.0/16 -``` - -::: - -```shell -kind create cluster --name platform --config ./config/kind-config.yaml -``` - -Export the internal kubeconfig of the Kind cluster to a file named `platform-int.kubeconfig` in the configuration folder. - -```shell -kind get kubeconfig --internal --name platform > ./kubeconfigs/platform-int.kubeconfig -``` - -### Create a bootstrapping configuration file (bootstrapper-config.yaml) in the configuration folder - -Replace `` and `` with your Git organization and repository name. -The environment can be set to the logical environment name (e.g. `dev`, `prod`, `live-eu-west`) that will be used in the Git repository to separate different environments. -The branch can be set to the desired branch name in the Git repository that will be used to store the desired state of the openMCP landscape. - -Get the latest version of the `github.com/openmcp-project/openmcp` root component: - -```shell -TAG=$(curl -s "https://api.github.com/repos/openmcp-project/openmcp/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) -echo "${TAG}" -``` - -In the bootstrapper configuration, replace `` with the latest version of the `github.com/openmcp/openmcp` root component: - -```yaml title="config/bootstrapper-config.yaml" -component: - location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: - -repository: - url: https://github.com// - pushBranch: - -environment: - -openmcpOperator: - config: {} -``` - -### Create a Git configuration file (git-config.yaml) in the configuration folder - -For GitHub use a personal access token with `repo` write permissions. -It is also possible to use a fine-grained token. In this case, it requires read and write permissions for `Contents`. - -```yaml title="config/git-config.yaml" -auth: - basic: - username: "" - password: "" -``` - -### Run the `openmcp-bootstrapper` CLI tool and deploy FluxCD to the Kind cluster - -```shell -docker run --rm --network kind -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} deploy-flux --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform-int.kubeconfig /config/bootstrapper-config.yaml -``` - -You should see output similar to the following: - -```shell -Info: Starting deployment of Flux controllers with config file: /config/bootstrapper-config.yaml. -Info: Ensure namespace flux-system exists -Info: Creating/updating git credentials secret flux-system/git -Info: Created/updated git credentials secret flux-system/git -Info: Creating working directory for gitops-templates -Info: Downloading templates -/tmp/openmcp.cloud.bootstrapper-3041773446/download: 9 file(s) with 691073 byte(s) written -Info: Arranging template files -Info: Arranged template files -Info: Applying templates from gitops-templates/fluxcd to deployment repository -Info: Kustomizing files in directory: /tmp/openmcp.cloud.bootstrapper-3041773446/repo/envs/dev/fluxcd -Info: Applying flux deployment objects -Info: Deployment of flux controllers completed -``` - -### Inspect the deployed FluxCD controllers and Kustomization - -Load the kubeconfig of the Kind cluster and check the deployed FluxCD controllers and the created GitRepository and Kustomization. - -```shell -kind get kubeconfig --name platform > ./kubeconfigs/platform.kubeconfig -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n flux-system -``` - -You should see output similar to the following: - -```shell -NAME READY STATUS RESTARTS AGE -helm-controller-648cdbf8d8-8jhnf 1/1 Running 0 9m37s -image-automation-controller-56df4c78dc-qwmfm 1/1 Running 0 9m35s -image-reflector-controller-56f69fcdc9-pgcgx 1/1 Running 0 9m35s -kustomize-controller-b4c4dcdc8-g49gc 1/1 Running 0 9m38s -notification-controller-59d754d599-w7fjp 1/1 Running 0 9m36s -source-controller-6b45b6464f-jbgb6 1/1 Running 0 9m38 -``` - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A -```` - -You should see output similar to the following: - -```shell -NAMESPACE NAME URL AGE READY STATUS -flux-system environments https://github.com// 86s False failed to checkout and determine revision: unable to clone 'https://github.com//': couldn't find remote ref "refs/heads/" -``` - -This error is expected as the branch does not exist yet in the Git repository. The `openmcp-bootstrapper` will create the branch in the next step. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME AGE READY STATUS -flux-system flux-system 3m15s False Source artifact not found, retrying in 30s -``` - -This error is also expected as the GitRepository does not exist yet. The `openmcp-bootstrapper` will create the GitRepository in the next step. - -### Run the `openmcp-bootstrapper` CLI tool to deploy openMCP to the Kind cluster - -Update the bootstrapping configuration file (bootstrapper-config.yaml) to include the kind cluster provider and the openmcp-operator configuration. - -```yaml title="config/bootstrapper-config.yaml" -component: - location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: - -repository: - url: https://github.com// - pushBranch: - -environment: - -providers: - clusterProviders: - - name: kind - config: - extraVolumeMounts: - - mountPath: /var/run/docker.sock - name: docker - extraVolumes: - - name: docker - hostPath: - path: /var/run/host-docker.sock - type: Socket - -openmcpOperator: - config: - managedControlPlane: - mcpClusterPurpose: mcp-worker - reconcileMCPEveryXDays: 7 - scheduler: - scope: Cluster - purposeMappings: - mcp: - template: - spec: - profile: kind - tenancy: Exclusive - mcp-worker: - template: - spec: - profile: kind - tenancy: Exclusive - platform: - template: - metadata: - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: kind - tenancy: Shared - onboarding: - template: - metadata: - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: kind - tenancy: Shared - workload: - tenancyCount: 20 - template: - spec: - profile: kind - tenancy: Shared -``` - -```shell -docker run --rm --network kind -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} manage-deployment-repo --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform-int.kubeconfig /config/bootstrapper-config.yaml -``` - -You should see output similar to the following: - -```shell -Info: Downloading component ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.20 -Info: Creating template transformer -Info: Downloading template resources -/tmp/openmcp.cloud.bootstrapper-2402093624/transformer/download/fluxcd: 9 file(s) with 691073 byte(s) written -/tmp/openmcp.cloud.bootstrapper-2402093624/transformer/download/openmcp: 8 file(s) with 6625 byte(s) written -Info: Transforming templates into deployment repository structure -Info: Fetching openmcp-operator component version -Info: Cloning deployment repository https://github.com/reshnm/template-test -Info: Checking out or creating branch kind -Info: Applying templates from "gitops-templates/fluxcd"/"gitops-templates/openmcp" to deployment repository -Info: Templating providers: clusterProviders=[{kind [123 34 101 120 116 114 97 86 111 108 117 109 101 77 111 117 110 116 115 34 58 91 123 34 109 111 117 110 116 80 97 116 104 34 58 34 47 118 97 114 47 114 117 110 47 100 111 99 107 101 114 46 115 111 99 107 34 44 34 110 97 109 101 34 58 34 100 111 99 107 101 114 34 125 93 44 34 101 120 116 114 97 86 111 108 117 109 101 115 34 58 91 123 34 104 111 115 116 80 97 116 104 34 58 123 34 112 97 116 104 34 58 34 47 118 97 114 47 114 117 110 47 104 111 115 116 45 100 111 99 107 101 114 46 115 111 99 107 34 44 34 116 121 112 101 34 58 34 83 111 99 107 101 116 34 125 44 34 110 97 109 101 34 58 34 100 111 99 107 101 114 34 125 93 44 34 118 101 114 98 111 115 105 116 121 34 58 34 100 101 98 117 103 34 125] map[extraVolumeMounts:[map[mountPath:/var/run/docker.sock name:docker]] extraVolumes:[map[hostPath:map[path:/var/run/host-docker.sock type:Socket] name:docker]] verbosity:debug]}], serviceProviders=[], platformServices=[], imagePullSecrets=[] -Info: Applying Custom Resource Definitions to deployment repository -/tmp/openmcp.cloud.bootstrapper-2402093624/repo/resources/openmcp/crds: 8 file(s) with 475468 byte(s) written -/tmp/openmcp.cloud.bootstrapper-2402093624/repo/resources/openmcp/crds: 1 file(s) with 1843 byte(s) written -Info: No extra manifest directory specified, skipping -Info: Committing and pushing changes to deployment repository -Info: Created commit: 287f9e88b905371bba412b5d0286ad02db0f4aac -Info: Running kustomize on /tmp/openmcp.cloud.bootstrapper-2402093624/repo/envs/dev -Info: Applying Kustomization manifest: default/bootstrap - -``` - -### Inspect the Git repository - -The desired state of the openMCP landscape has now been created in the Git repository and should look similar to the following structure: - -```shell -. -├── envs -│   └── dev -│   ├── fluxcd -│   │   ├── flux-kustomization.yaml -│   │   ├── gitrepo.yaml -│   │   └── kustomization.yaml -│   ├── kustomization.yaml -│   ├── openmcp -│   │   ├── config -│   │   │   └── openmcp-operator-config.yaml -│   │   └── kustomization.yaml -│   └── root-kustomization.yaml -└── resources - ├── fluxcd - │   ├── components.yaml - │   ├── flux-kustomization.yaml - │   ├── gitrepo.yaml - │   └── kustomization.yaml - ├── kustomization.yaml - ├── openmcp - │   ├── cluster-providers - │   │   └── kind.yaml - │   ├── crds - │   │   ├── clusters.openmcp.cloud_accessrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusterprofiles.yaml - │   │   ├── clusters.openmcp.cloud_clusterrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusters.yaml - │   │   ├── kind.clusters.openmcp.cloud_providerconfigs.yaml - │   │   ├── openmcp.cloud_clusterproviders.yaml - │   │   ├── openmcp.cloud_platformservices.yaml - │   │   └── openmcp.cloud_serviceproviders.yaml - │   ├── deployment.yaml - │   ├── kustomization.yaml - │   ├── namespace.yaml - │   └── rbac.yaml - └── root-kustomization.yaml -``` - -The `envs/` folder contains the Kustomization files that are used by FluxCD to deploy openMCP to the Kind cluster. -The `resources` folder contains the base resources that are used by the Kustomization files in the `envs/` folder. - -## Inspect the Kustomizations in the Kind cluster - -Force an update of the GitRepository and Kustomization in the Kind cluster to pick up the changes made in the Git repository. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system annotate gitrepository environments reconcile.fluxcd.io/requestedAt="$(date +%s)" -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system patch kustomization flux-system --type merge -p '{"spec":{"force":true}}' -``` - -Get the status of the GitRepository in the Kind cluster. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME URL AGE READY STATUS -flux-system environments https://github.com// 9m6s True stored artifact for revision 'docs@sha1:...' -``` - -So we have now successfully configured FluxCD to watch for changes in the specified GitHub repository, using the `environments` custom resource of kind `GitRepository`. -Now let's get the status of the Kustomization in the Kind cluster. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME AGE READY STATUS -default bootstrap 5m31s True Applied revision: docs@sha1:... -flux-system flux-system 10m True Applied revision: docs@sha1:... -``` - -You can see that there are now two Kustomizations in the Kind cluster. -The `flux-system` Kustomization is used to deploy the FluxCD controllers and the `bootstrap` Kustomization is used to deploy openMCP to the Kind cluster. - -### Inspect the deployed openMCP components in the Kind cluster - -Now check the deployed openMCP components. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n openmcp-system -``` - -You should see output similar to the following: - -```shell -NAME READY STATUS RESTARTS AGE -cp-kind-6b4886b7cf-z54pg 1/1 Running 0 20s -cp-kind-init-msqg7 0/1 Completed 0 27s -openmcp-operator-5f784f47d7-nfg65 1/1 Running 0 34s -ps-managedcontrolplane-668c99c97c-9jltx 1/1 Running 0 4s -ps-managedcontrolplane-init-49rx2 0/1 Completed 0 27s -``` - -So now, the openmcp-operator, the managedcontrolplane platform service and the cluster provider kind are running. -You are now ready to create and manage clusters using openMCP. - -### Get Access to the Onboarding Cluster - -The openmcp-operator should now have created a `onboarding Cluster` resource on the platform cluster that represents the onboarding cluster. -The onboarding cluster is a special cluster that is used to create new managed control planes. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get clusters.clusters.openmcp.cloud -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME PURPOSES PHASE VERSION PROVIDER AGE -openmcp-system onboarding ["onboarding"] Ready 11m -``` - -Now you can retrieve the kubeconfig of the onboarding cluster. -Use `kind` to retrieve the list of available clusters. - -```shell -kind get clusters -``` - -You should see output similar to the following: - -```shell -onboarding.12345678 -platform -``` - -You can now see the new onboarding cluster. -Get the kubeconfig of the onboarding cluster and save it to a file named `onboarding.kubeconfig` in the configuration folder. -Please replace `onboarding.12345678` with the actual name of your onboarding cluster. - -```shell -kind get kubeconfig --name onboarding.12345678 > ./kubeconfigs/onboarding.kubeconfig -``` - -### Create a Managed Control Plane - -Create a file named `my-mcp.yaml` with the following content in the configuration folder: - -```yaml title="config/my-mcp.yaml" -apiVersion: core.openmcp.cloud/v2alpha1 -kind: ManagedControlPlaneV2 -metadata: - name: my-mcp - namespace: default -spec: - iam: {} -``` - -Apply the file to the onboarding cluster: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig apply -f ./config/my-mcp.yaml -``` - -The openmcp-operator should start to create the necessary resources in order to create the managed control plane. As a result, a new `Managed Control Plane` should be available soon. -You can check the status of the Managed Control Plane using the following command: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get managedcontrolplanev2 -n default my-mcp -o yaml -``` - -You should see output similar to the following: - -```yaml -apiVersion: core.openmcp.cloud/v2alpha1 -kind: ManagedControlPlaneV2 -metadata: - finalizers: - - core.openmcp.cloud/mcp - - request.clusters.openmcp.cloud/sample - name: sample - namespace: default -spec: - iam: {} -status: - conditions: - - lastTransitionTime: "2025-09-16T13:03:55Z" - message: All accesses are ready - observedGeneration: 1 - reason: AllAccessReady_True - status: "True" - type: AllAccessReady - - lastTransitionTime: "2025-09-16T13:03:55Z" - message: Cluster conditions have been synced to MCP - observedGeneration: 1 - reason: ClusterConditionsSynced_True - status: "True" - type: ClusterConditionsSynced - - lastTransitionTime: "2025-09-16T13:03:55Z" - message: ClusterRequest is ready - observedGeneration: 1 - reason: ClusterRequestReady_True - status: "True" - type: ClusterRequestReady - - lastTransitionTime: "2025-09-16T13:03:50Z" - message: "" - observedGeneration: 1 - reason: Meta_True - status: "True" - type: Meta - observedGeneration: 1 - phase: Ready -``` - -You should see that the Managed Control Plane is in phase `Ready`. -The openmcp-operator should now have created a new Kind cluster that represents the Managed Control Plane. -You can check the list of available Kind clusters using the following command: - -```shell -kind get clusters -``` - -You should see output similar to the following: - -```shell -mcp-worker-abcde.87654321 -onboarding.12345678 -platform -``` - -You can now get the kubeconfig of the managed control plane and save it to a file named `my-mcp.kubeconfig` in the kubeconfigs folder. Please replace `mcp-worker-abcde.87654321` with the actual name of your managed control plane cluster. - -```shell -kind get kubeconfig --name mcp-worker-abcde.87654321 > ./kubeconfigs/my-mcp.kubeconfig -``` - -You can now use the kubeconfig to access the Managed Control Plane cluster. - -```shell -kubectl --kubeconfig ./kubeconfigs/my-mcp.kubeconfig get namespaces -``` - -### Deploy the Crossplane Service Provider - -Update the bootstrapping configuration file (bootstrapper-config.yaml) to include the crossplane service provider. - -```yaml title="config/bootstrapper-config.yaml" -component: - location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: - -repository: - url: https://github.com// - pushBranch: - -environment: - -providers: - clusterProviders: - - name: kind - config: - extraVolumeMounts: - - mountPath: /var/run/docker.sock - name: docker - extraVolumes: - - name: docker - hostPath: - path: /var/run/host-docker.sock - type: Socket - serviceProviders: - - name: crossplane - -openmcpOperator: - config: - managedControlPlane: - mcpClusterPurpose: mcp-worker - reconcileMCPEveryXDays: 7 - scheduler: - scope: Cluster - purposeMappings: - mcp: - template: - spec: - profile: kind - tenancy: Exclusive - mcp-worker: - template: - spec: - profile: kind - tenancy: Exclusive - platform: - template: - metadata: - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: kind - tenancy: Shared - onboarding: - template: - metadata: - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: kind - tenancy: Shared - workload: - tenancyCount: 20 - template: - spec: - profile: kind - tenancy: Shared -``` - -Create a new folder named `extra-manifests` in the configuration folder. Then create a file named `crossplane-provider.yaml` with the following content, and save it in the new `extra-manifests` folder. - -:::info -Note that service provider crossplane only supports the installation of crossplane from an OCI registry. Replace the chart locations in the `ProviderConfig` with the OCI registry where you mirror your crossplane chart versions. OpenMCP will provide this as part of an open source [Releasechannel](https://github.com/openmcp-project/backlog/issues/323) in an upcoming update. -::: - -```yaml title="config/extra-manifests/crossplane-provider.yaml" -apiVersion: crossplane.services.openmcp.cloud/v1alpha1 -kind: ProviderConfig -metadata: - name: default -spec: - versions: - - version: v2.0.2 - chart: - url: ghcr.io/openmcp-project/charts/crossplane:2.0.2 - image: - url: xpkg.crossplane.io/crossplane/crossplane:v2.0.2 - - version: v1.20.1 - chart: - url: ghcr.io/openmcp-project/charts/crossplane:1.20.1 - image: - url: xpkg.crossplane.io/crossplane/crossplane:v1.20.1 - providers: - availableProviders: - - name: provider-kubernetes - package: xpkg.upbound.io/upbound/provider-kubernetes - versions: - - v0.16.0 -``` - -Run the `openmcp-bootstrapper` CLI tool to update the Git repository and deploy the crossplane service provider to the Kind cluster. - -```shell -docker run --rm --network kind -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} manage-deployment-repo --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform-int.kubeconfig --extra-manifest-dir /config/extra-manifests /config/bootstrapper-config.yaml -``` - -See the `--extra-manifest-dir` parameter that points to the folder containing the extra manifest file created in the previous step. All manifest files in this folder will be added to the Kustomization used by FluxCD to deploy openMCP to the Kind cluster. - -The git repository should now be updated: - -```shell -. -├── envs -│   └── dev -│   ├── fluxcd -│   │   ├── flux-kustomization.yaml -│   │   ├── gitrepo.yaml -│   │   └── kustomization.yaml -│   ├── kustomization.yaml -│   ├── openmcp -│   │   ├── config -│   │   │   └── openmcp-operator-config.yaml -│   │   └── kustomization.yaml -│   └── root-kustomization.yaml -└── resources - ├── fluxcd - │   ├── components.yaml - │   ├── flux-kustomization.yaml - │   ├── gitrepo.yaml - │   └── kustomization.yaml - ├── kustomization.yaml - ├── openmcp - │   ├── cluster-providers - │   │   └── kind.yaml - │   ├── crds - │   │   ├── clusters.openmcp.cloud_accessrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusterprofiles.yaml - │   │   ├── clusters.openmcp.cloud_clusterrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusters.yaml - │   │   ├── crossplane.services.openmcp.cloud_providerconfigs.yaml - │   │   ├── kind.clusters.openmcp.cloud_providerconfigs.yaml - │   │   ├── openmcp.cloud_clusterproviders.yaml - │   │   ├── openmcp.cloud_platformservices.yaml - │   │   └── openmcp.cloud_serviceproviders.yaml - │   ├── deployment.yaml - │   ├── extra - │   │   └── crossplane-providers.yaml - │   ├── kustomization.yaml - │   ├── namespace.yaml - │   ├── rbac.yaml - │   └── service-providers - │   └── crossplane.yaml - └── root-kustomization.yaml -``` - -After a while, the Kustomization in the Kind cluster should be updated and the crossplane service provider should be deployed: -You can force an update of the Kustomization in the Kind cluster to pick up the changes made in the Git repository. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system annotate gitrepository environments reconcile.fluxcd.io/requestedAt="$(date +%s)" -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n default patch kustomization bootstrap --type merge -p '{"spec":{"force":true}}' -``` - -List the pods in the `openmcp-system` namespace again: - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n openmcp-system -```` - -You should see output similar to the following: - -```shell -NAME READY STATUS RESTARTS AGE -cp-kind-6b4886b7cf-z54pg 1/1 Running 0 18m -cp-kind-init-msqg7 0/1 Completed 0 18m -openmcp-operator-5f784f47d7-nfg65 1/1 Running 0 18m -ps-managedcontrolplane-668c99c97c-9jltx 1/1 Running 0 18m -ps-managedcontrolplane-init-49rx2 0/1 Completed 0 18m -sp-crossplane-6b8cccc775-9hx98 1/1 Running 0 105s -sp-crossplane-init-6hvf4 0/1 Completed 0 2m11s -``` - -You should see that the crossplane service provider is running. This means that from now on, the openMCP is able to provide Crossplane service instances, using the new service provider Crossplane. - -### Create a Crossplane service instance on the onboarding cluster - -Create a file named `crossplane-instance.yaml` with the following content in the configuration folder: - -```yaml title="config/crossplane-instance.yaml" -apiVersion: crossplane.services.openmcp.cloud/v1alpha1 -kind: Crossplane -metadata: - name: my-mcp - namespace: default -spec: - version: v1.20.0 - providers: - - name: provider-kubernetes - version: v0.16.0 -``` - -Apply the file to onboarding cluster: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig apply -f ./config/crossplane-instance.yaml -``` - -The Crossplane service provider should now start to create the necessary resources for the new Crossplane instance. As a result, a new Crossplane service instance should soon be available. -You can check the status of the Crossplane instance using the following command: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get crossplane -n default my-mcp -o yaml -``` - -After a while, you should see output similar to the following: - -```yaml -apiVersion: crossplane.services.openmcp.cloud/v1alpha1 -kind: Crossplane -metadata: - finalizers: - - openmcp.cloud/finalizers - generation: 1 - name: sample - namespace: default -spec: - providers: - - name: provider-kubernetes - version: v0.16.0 - version: v1.20.0 -status: - conditions: - - lastTransitionTime: "2025-09-16T14:09:56Z" - message: Crossplane is healthy. - reason: Healthy - status: "True" - type: CrossplaneReady - - lastTransitionTime: "2025-09-16T14:10:01Z" - message: ProviderKubernetes is healthy. - reason: Healthy - status: "True" - type: ProviderKubernetesReady - observedGeneration: 0 - phase: "" -``` - -Crossplane and the provider Kubernetes should now be available on the mcp cluster. - -```shell -kubectl --kubeconfig ./kubeconfigs/my-mcp.kubeconfig api-resources | grep 'crossplane\|kubernetes' -``` - -## Example using the Gardener Cluster Provider - -### Requirements - -* A running Gardener installation (see the [Gardener documentation](https://gardener.cloud/docs/) for more information on Gardener) -* A Gardener project in which the clusters will be created -* An infrastructure secret in the Gardener project (see the [Gardener documentation](https://gardener.cloud/docs/getting-started/project/#infrastructure-secrets) for more information on how to create an infrastructure secret) -* Kubectl (see the [Kubectl installation guide](https://kubernetes.io/docs/tasks/tools/#kubectl) for more information on how to install kubectl) -* If the Gardener installation is using OIDC for authentication, install the [OIDC kubectl plugin](https://github.com/int128/kubelogin) -* Good understanding of Gardener and how to create Gardener Shoot clusters and Service Accounts in Gardener Projects. - -### Create a configuration folder - -Create a directory that will be used to store the configuration files and the kubeconfig files. -To keep this example simple, we will use a single directory named `config` in the current working directory. - -```shell -mkdir config -``` - -All following examples will use the `config` directory as the configuration directory. If you use a different directory, replace all occurrences of `config` with your desired directory path. - -Create a directory named `kubeconfigs` in the configuration folder to store the kubeconfig files of the created clusters. - -```shell -mkdir kubeconfigs -``` - -### Create a Gardener Shoot for the Platform Cluster - -openMCP requires a running Kubernetes cluster that acts as the platform cluster. -The platform cluster hosts the openmcp-operator and all service providers, cluster providers and platform services. -In this example, we will create a Gardener Shoot cluster that acts as the platform cluster. See the [Gardener documentation](https://gardener.cloud/docs/getting-started/shoots/) for more information on how to create a Gardener Shoot cluster. - -Create a script folder named `scripts`: - -```shell -mkdir scripts -``` - -Create a file named `get-shoot-kubeconfig.sh` in the `scripts` folder with the following content: - -```shell title="scripts/get-shoot-kubeconfig.sh" -#!/usr/bin/env bash - -GARDENER_SECRET=$1 -NAMESPACE="garden-$2" -SHOOT_NAME=$3 - -REQUEST_PATH="$(mktemp -d)" -REQUEST="${REQUEST_PATH}/admin-kubeconfig-request.json" - -echo "{ \"apiVersion\": \"authentication.gardener.cloud/v1alpha1\", \"kind\": \"AdminKubeconfigRequest\", \"spec\": { \"expirationSeconds\": 7776000 } }" > ${REQUEST} 2>/dev/null - -KUBECONFIG=$(kubectl --kubeconfig "${GARDENER_SECRET}" create \ - -f ${REQUEST} \ - --raw /apis/core.gardener.cloud/v1beta1/namespaces/${NAMESPACE}/shoots/${SHOOT_NAME}/adminkubeconfig 2>/dev/null | jq -r ".status.kubeconfig" | base64 -d) - - -echo "${KUBECONFIG}" -``` - -Make the script executable: - -```shell -chmod +x ./scripts/get-shoot-kubeconfig.sh -``` - -In order to execute this script, you need a kubeconfig file that has access to the Gardener installation. This can be aquired by navigating to the Gardener dashboard, then selecting your user (icon in the upper right corner) -> click 'My Account' and under `Access` download the Kubeconfig file. - -Alternatively, you can create a service account with the `Admin` role in the Gardener project and then retrieve the kubeconfig for the service account. See the [Gardener documentation](https://gardener.cloud/docs/getting-started/project/#service-accounts) for more information on how to create a service account. - -Now, create a new Gardener Shoot cluster in your Gardener project using the Gardener dashboard or the Gardener API via kubectl. The name of the Shoot cluster shall be `platform`. -Please consult the [Gardener documentation](https://gardener.cloud/docs/getting-started/shoots/) for more information on how to create a Gardener Shoot cluster. - -Download the admin kubeconfig of the `platform` Shoot cluster using the script created above (`get-shoot-kubeconfig.sh`) and save it to a file named `platform.kubeconfig` in the `kubeconfigs` folder. - -```shell -./scripts/get-shoot-kubeconfig.sh platform > ./kubeconfigs/platform.kubeconfig -``` - -### Create a bootstrapping configuration file (bootstrapper-config.yaml) in the configuration folder - -Replace `` and `` with your Git organization and repository name. -The environment can be set to the logical environment name (e.g. `dev`, `prod`, `live-eu-west`) that will be used in the Git repository to separate different environments. -The branch can be set to the desired branch name in the Git repository that will be used to store the desired state of the openMCP landscape. - -Get the latest version of the `github.com/openmcp/openmcp` root component: - -```shell -TAG=$(curl -s "https://api.github.com/repos/openmcp-project/openmcp/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) -echo "${TAG}" -``` - -In the bootstrapper configuration, replace `` with the latest version of the `github.com/openmcp-project/openmcp` root component: - -```yaml title="config/bootstrapper-config.yaml" -component: - location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: - -repository: - url: https://github.com// - pushBranch: - -environment: - -openmcpOperator: - config: {} -``` - -### Create a Git configuration file (git-config.yaml) in the configuration folder - -For GitHub use a personal access token with `repo` write permissions. -It is also possible to use a fine-grained token. In this case, it requires read and write permissions for `Contents`. - -```yaml title="config/git-config.yaml" -auth: - basic: - username: "" - password: "" -``` - -### Run the `openmcp-bootstrapper` CLI tool to deploy FluxCD to the Platform Cluster - -Run the `openmcp-bootstrapper` CLI tool to deploy FluxCD to the `platform` Gardener Shoot cluster: - -```shell -docker run --rm -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} deploy-flux --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform.kubeconfig /config/bootstrapper-config.yaml -``` - -You should see output similar to the following: - -```shell -Info: Starting deployment of Flux controllers with config file: /config/bootstrapper-config.yaml. -Info: Ensure namespace flux-system exists -Info: Creating/updating git credentials secret flux-system/git -Info: Created/updated git credentials secret flux-system/git -Info: Creating working directory for gitops-templates -Info: Downloading templates -/tmp/openmcp.cloud.bootstrapper-3041773446/download: 9 file(s) with 691073 byte(s) written -Info: Arranging template files -Info: Arranged template files -Info: Applying templates from gitops-templates/fluxcd to deployment repository -Info: Kustomizing files in directory: /tmp/openmcp.cloud.bootstrapper-3041773446/repo/envs/dev/fluxcd -Info: Applying flux deployment objects -Info: Deployment of flux controllers completed -``` - -### Inspect the deployed FluxCD controllers and Kustomization - -Load the kubeconfig of the Kind cluster and check the deployed FluxCD controllers and the created GitRepository and Kustomization. - -```shell -kind get kubeconfig --name platform > ./kubeconfigs/platform.kubeconfig -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n flux-system -``` - -You should see output similar to the following: - -```shell -NAME READY STATUS RESTARTS AGE -helm-controller-648cdbf8d8-8jhnf 1/1 Running 0 9m37s -image-automation-controller-56df4c78dc-qwmfm 1/1 Running 0 9m35s -image-reflector-controller-56f69fcdc9-pgcgx 1/1 Running 0 9m35s -kustomize-controller-b4c4dcdc8-g49gc 1/1 Running 0 9m38s -notification-controller-59d754d599-w7fjp 1/1 Running 0 9m36s -source-controller-6b45b6464f-jbgb6 1/1 Running 0 9m38s -``` - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A -```` - -You should see output similar to the following: - -```shell -NAMESPACE NAME URL AGE READY STATUS -flux-system environments https://github.com// 86s False failed to checkout and determine revision: unable to clone 'https://github.com//': couldn't find remote ref "refs/heads/" -``` - -This error is expected as the branch does not exist yet in the Git repository. The `openmcp-bootstrapper` will create the branch in the next step. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME AGE READY STATUS -flux-system flux-system 3m15s False Source artifact not found, retrying in 30s -``` - -This error is also expected as the GitRepository does not exist yet. The `openmcp-bootstrapper` will create the GitRepository in the next step. - -### Run the `openmcp-bootstrapper` CLI tool to deploy openMCP to the Kind cluster - -Update the bootstrapping configuration file (bootstrapper-config.yaml) to include the Gardener cluster provider and the openmcp-operator configuration. - -Please replace `` with the logical environment name (e.g. `dev`, `prod`, `live-eu-west`) that will be used in the Git repository to separate different environments. Notice that the same environment name must be used in the `environment` field and in the scheduler profiles. - -```yaml title="config/bootstrapper-config.yaml" -component: - location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: - -repository: - url: https://github.com// - pushBranch: - -environment: - -providers: - clusterProviders: - - name: gardener - -openmcpOperator: - config: - managedControlPlane: - mcpClusterPurpose: mcp-worker - reconcileMCPEveryXDays: 7 - scheduler: - scope: Cluster - purposeMappings: - mcp-worker: - template: - metadata: - namespace: openmcp-system - spec: - profile: .gardener.shoot-small - tenancy: Exclusive - platform: - template: - metadata: - namespace: openmcp-system - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: .gardener.shoot-small - tenancy: Shared - onboarding: - template: - metadata: - namespace: openmcp-system - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: .gardener.shoot-workerless - tenancy: Shared - workload: - tenancyCount: 20 - template: - metadata: - namespace: openmcp-system - spec: - profile: .gardener.shoot-small - tenancy: Shared -``` - -Create a directory named `extra-manifests` in the configuration folder. - -```shell -mkdir ./config/extra-manifests -``` - -In the `extra-manifests` folder, create a file named `gardener-landscape.yaml` with the following content: - -```yaml title="config/extra-manifests/gardener-landscape.yaml" -apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 -kind: Landscape -metadata: - name: gardener-landscape -spec: - access: - secretRef: - name: gardener-landscape-kubeconfig - namespace: openmcp-system -``` - -The gardener landscape configuration requires a secret that contains the kubeconfig to access the Gardener project. For that purpose, create a secret named `gardener-landscape-kubeconfig` in the `openmcp-system` namespace of the platform cluster that contains the kubeconfig file that has access to the Gardener installation. -See the [Gardener documentation](https://gardener.cloud/docs/dashboard/automated-resource-management/#create-a-service-account) on how to create a service account in the Gardener project using the Gardener dashboard. -Create a service account with at least the `admin` role in the Gardener project. Then [download](https://gardener.cloud/docs/dashboard/automated-resource-management/#use-the-service-account) the kubeconfig for the service account and save it to a file named `./kubeconfigs/gardener-landscape.kubeconfig`. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig create namespace openmcp-system -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig create secret generic gardener-landscape-kubeconfig --from-file=kubeconfig=./kubeconfigs/gardener-landscape.kubeconfig -n openmcp-system -``` - -The kubeconfig content can be retrieved from the Gardener dashboard or by creating a service account in the Gardener project. See the [Gardener documentation](https://gardener.cloud/docs/getting-started/project/#service-accounts) for more information on how to create a service account. -The service account requires at least the `admin` role in the Gardener project. - -In the `extra-manifests` folder, create a file named `gardener-cluster-provider-shoot-small.yaml` with the following content: - - - - -```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-small.yaml" -apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 -kind: ProviderConfig -metadata: - name: shoot-small -spec: - landscapeRef: - name: gardener-landscape - project: - providerRef: - name: gardener - shootTemplate: - spec: - cloudProfile: - kind: CloudProfile - name: gcp - kubernetes: - version: "" # e.g. "1.32" - maintenance: - autoUpdate: - kubernetesVersion: true - timeWindow: - begin: 220000+0200 - end: 230000+0200 - networking: - nodes: 10.180.0.0/16 - type: calico - provider: - controlPlaneConfig: - apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 - kind: ControlPlaneConfig - zone: # e.g. europe-west1-c - infrastructureConfig: - apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 - kind: InfrastructureConfig - networks: - workers: 10.180.0.0/16 - type: gcp - workers: - - cri: - name: containerd - machine: - architecture: amd64 - image: - name: gardenlinux - version: "" # e.g. "1592.9.0" - type: n1-standard-2 - maxSurge: 1 - maximum: 5 - minimum: 1 - name: default-worker - volume: - size: 50Gi - type: pd-balanced - zones: - - # e.g. europe-west1-c - purpose: evaluation - region: # e.g. europe-west1 - secretBindingName: -``` - - - -```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-small.yaml" -apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 -kind: ProviderConfig -metadata: - name: shoot-small -spec: - landscapeRef: - name: gardener-landscape - project: - providerRef: - name: gardener - shootTemplate: - spec: - cloudProfile: - kind: CloudProfile - name: aws - kubernetes: - version: "" # e.g. "1.32" - maintenance: - autoUpdate: - kubernetesVersion: true - timeWindow: - begin: 220000+0200 - end: 230000+0200 - networking: - type: calico - nodes: 10.180.0.0/16 - provider: - controlPlaneConfig: - apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 - kind: ControlPlaneConfig - cloudControllerManager: - useCustomRouteController: true - storage: - managedDefaultClass: true - infrastructureConfig: - apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 - kind: InfrastructureConfig - networks: - vpc: - cidr: 10.180.0.0/16 - zones: - - name: # e.g. eu-west-1a - workers: 10.180.0.0/19 - public: 10.180.32.0/20 - internal: 10.180.48.0/20 - type: aws - workers: - - cri: - name: containerd - machine: - architecture: amd64 - image: - name: gardenlinux - version: "" # e.g. "1592.9.0" - type: m5.large - maxSurge: 1 - maximum: 5 - minimum: 1 - name: default-worker - volume: - size: 50Gi - type: gp3 - zones: - - # e.g. eu-west-1a - purpose: evaluation - region: # e.g. eu-west-1 - secretBindingName: -``` - - - - -In the `extra-manifests` folder, create a file named `gardener-cluster-provider-shoot-workerless.yaml` with the following content: - - - - -```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-workerless.yaml" -apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 -kind: ProviderConfig -metadata: - name: shoot-workerless -spec: - landscapeRef: - name: gardener-landscape - project: - providerRef: - name: gardener - shootTemplate: - spec: - cloudProfile: - kind: CloudProfile - name: gcp - kubernetes: - version: "" # e.g. "1.32" - maintenance: - autoUpdate: - kubernetesVersion: true - timeWindow: - begin: 220000+0200 - end: 230000+0200 - provider: - type: gcp - purpose: evaluation - region: # eg europe-west1 -``` - - - -```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-workerless.yaml" -apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 -kind: ProviderConfig -metadata: - name: shoot-workerless -spec: - landscapeRef: - name: gardener-landscape - project: - providerRef: - name: gardener - shootTemplate: - spec: - cloudProfile: - kind: CloudProfile - name: aws - kubernetes: - version: "" # e.g. "1.32" - maintenance: - autoUpdate: - kubernetesVersion: true - timeWindow: - begin: 220000+0200 - end: 230000+0200 - provider: - type: aws - purpose: evaluation - region: # e.g. eu-west-1 -``` - - - - -Replace `` with the name of your Gardener project and `` with the name of the secret binding that contains the infrastructure secret for your Gardener project. - -Replace also `` with the desired Kubernetes version (e.g. `1.32`), `` with the desired Garden Linux version (e.g. `1592.9.0`), `` with the desired region (e.g. `europe-west1`), and `` with the desired zone (e.g. `europe-west1-c`). - -:::info -Please adjust the shoot configuration based on your specific needs, e.g. change `Evaluation` to `Production` as purpose, if you are planning to use the MCP for productive purposes. For all the details reg. Shoot configuration, please consult the respective Gardener documentation. -::: - -Now run the `openmcp-bootstrapper` CLI tool to update the Git repository and deploy openMCP to the `platform` Gardener Shoot cluster: - -```shell -docker run --rm -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} manage-deployment-repo --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform.kubeconfig --extra-manifest-dir /config/extra-manifests /config/bootstrapper-config.yaml -``` - -You should see output similar to the following: - -```shell -Info: Downloading component ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.25 -Info: Creating template transformer -Info: Downloading template resources -/tmp/openmcp.cloud.bootstrapper-245193548/transformer/download/fluxcd: 9 file(s) with 691073 byte(s) written -/tmp/openmcp.cloud.bootstrapper-245193548/transformer/download/openmcp: 8 file(s) with 6625 byte(s) written -Info: Transforming templates into deployment repository structure -Info: Fetching openmcp-operator component version -Info: Cloning deployment repository https://github.com/reshnm/openmcp-deployment -Info: Checking out or creating branch gardener -Info: Applying templates from "gitops-templates/fluxcd"/"gitops-templates/openmcp" to deployment repository -Info: Templating providers: clusterProviders=[{gardener [] map[]}], serviceProviders=[], platformServices=[], imagePullSecrets=[] -Info: Applying Custom Resource Definitions to deployment repository -/tmp/openmcp.cloud.bootstrapper-245193548/repo/resources/openmcp/crds: 8 file(s) with 484832 byte(s) written -/tmp/openmcp.cloud.bootstrapper-245193548/repo/resources/openmcp/crds: 3 file(s) with 198428 byte(s) written -Info: Applying extra manifests from /config/extra-manifests to deployment repository -Info: Committing and pushing changes to deployment repository -Info: Created commit: ee2b6ef079808fbc198b4f6eced1afb89f64d1d1 -Info: Running kustomize on /tmp/openmcp.cloud.bootstrapper-245193548/repo/envs/dev -Info: Applying Kustomization manifest: default/bootstrap -``` - -### Inspect the Git repository - -The desired state of the openMCP landscape has now been created in the Git repository and should look similar to the following structure: - -```shell -. -├── envs -│   └── dev -│   ├── fluxcd -│   │   ├── flux-kustomization.yaml -│   │   ├── gitrepo.yaml -│   │   └── kustomization.yaml -│   ├── kustomization.yaml -│   ├── openmcp -│   │   ├── config -│   │   │   └── openmcp-operator-config.yaml -│   │   └── kustomization.yaml -│   └── root-kustomization.yaml -└── resources - ├── fluxcd - │   ├── components.yaml - │   ├── flux-kustomization.yaml - │   ├── gitrepo.yaml - │   └── kustomization.yaml - ├── kustomization.yaml - ├── openmcp - │   ├── cluster-providers - │   │   └── gardener.yaml - │   ├── crds - │   │   ├── clusters.openmcp.cloud_accessrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusterprofiles.yaml - │   │   ├── clusters.openmcp.cloud_clusterrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusters.yaml - │   │   ├── gardener.clusters.openmcp.cloud_clusterconfigs.yaml - │   │   ├── gardener.clusters.openmcp.cloud_landscapes.yaml - │   │   ├── gardener.clusters.openmcp.cloud_providerconfigs.yaml - │   │   ├── openmcp.cloud_clusterproviders.yaml - │   │   ├── openmcp.cloud_platformservices.yaml - │   │   └── openmcp.cloud_serviceproviders.yaml - │   ├── deployment.yaml - │   ├── extra - │   │   ├── gardener-cluster-provider-shoot-small.yaml - │   │   ├── gardener-cluster-provider-shoot-workerless.yaml - │   │   └── gardener-landscape.yaml - │   ├── kustomization.yaml - │   ├── namespace.yaml - │   └── rbac.yaml - └── root-kustomization.yaml -``` - -The `envs/` folder contains the Kustomization files that are used by FluxCD to deploy openMCP to the platform cluster. -The `resources` folder contains the base resources that are used by the Kustomization files in the `envs/` folder. - -## Inspect the Kustomizations in the platform cluster - -Force an update of the GitRepository and Kustomization in the Kind cluster to pick up the changes made in the Git repository. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system annotate gitrepository environments reconcile.fluxcd.io/requestedAt="$(date +%s)" -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system patch kustomization flux-system --type merge -p '{"spec":{"force":true}}' -``` - -Get the status of the GitRepository in the platform cluster. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME URL AGE READY STATUS -flux-system environments https://github.com// 9m6s True stored artifact for revision 'docs@sha1:...' -``` - -So we have now successfully configured FluxCD to watch for changes in the specified GitHub repository, using the `environments` custom resource of kind `GitRepository`. -Now let's get the status of the Kustomization in the Kind cluster. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME AGE READY STATUS -default bootstrap 5m31s True Applied revision: docs@sha1:... -flux-system flux-system 10m True Applied revision: docs@sha1:... -``` - -You can see that there are now two Kustomizations in the platform cluster. -The `flux-system` Kustomization is used to deploy the FluxCD controllers and the `bootstrap` Kustomization is used to deploy openMCP to the platform cluster. - -### Inspect the deployed openMCP components on the platform cluster - -Now check the deployed openMCP components. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n openmcp-system -``` - -You should see output similar to the following: - -```shell -NAME READY STATUS RESTARTS AGE -cp-gardener-7f77684ffb-gw4jg 1/1 Running 0 35m -cp-gardener-init-wxnt4 0/1 Completed 0 35m -openmcp-operator-785b967f66-h2dlh 1/1 Running 0 67m -ps-managedcontrolplane-5b77749f7b-mtffp 1/1 Running 0 64m -ps-managedcontrolplane-init-pklrl 0/1 Completed 0 67m -``` - -So now, the openmcp-operator, the managedcontrolplane platform service and the cluster provider gardener are running. -You are now ready to create and manage clusters using openMCP. - -### Inspect cluster profiles and clusters - -Based on the provider configuration for the Gardener cluster provider, two cluster profiles should have been created: `dev.gardener.shoot-small` and `dev.gardener.shoot-workerless`. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get clusterprofiles.clusters.openmcp.cloud -``` - -You should see output similar to the following: - -```shell -NAME PROVIDER CONFIG -dev.gardener.shoot-small gardener shoot-small -dev.gardener.shoot-workerless gardener shoot-workerless -``` - -As you can see, these names match the profile names used in the openmcp-operator configuration. The nameing convention is `..`. - -Inspecting a cluster profile, shows the supported kubernetes versions: - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get clusterprofiles.clusters.openmcp.cloud dev.gardener.shoot-small -o yaml -``` - -You should see output similar to the following: - -```yaml -apiVersion: clusters.openmcp.cloud/v1alpha1 -kind: ClusterProfile -metadata: - creationTimestamp: "2025-10-01T06:38:48Z" - generation: 1 - name: dev.gardener.shoot-small - resourceVersion: "173288" - uid: 926aa91c-f021-41f7-b97c-dc7eaf0e19bf -spec: - providerConfigRef: - name: shoot-small - providerRef: - name: gardener - supportedVersions: - - version: 1.33.3 - - deprecated: true - version: 1.33.2 - - version: 1.32.7 - - deprecated: true - version: 1.32.6 - - deprecated: true - version: 1.32.5 - - deprecated: true - version: 1.32.4 - - deprecated: true - version: 1.32.3 - - deprecated: true - version: 1.32.2 - - version: 1.31.11 - - deprecated: true - version: 1.31.10 - - deprecated: true - version: 1.31.9 - - deprecated: true - version: 1.31.8 - - deprecated: true - version: 1.31.7 - - deprecated: true - version: 1.31.6 - - deprecated: true - version: 1.31.5 - - deprecated: true - version: 1.31.4 - - deprecated: true - version: 1.31.3 - - deprecated: true - version: 1.31.2 - - version: 1.30.14 - - deprecated: true - version: 1.30.13 - - deprecated: true - version: 1.30.12 - - deprecated: true - version: 1.30.11 - - deprecated: true - version: 1.30.10 - - deprecated: true - version: 1.30.9 - - deprecated: true - version: 1.30.8 - - deprecated: true - version: 1.30.7 - - deprecated: true - version: 1.30.6 - - deprecated: true - version: 1.30.5 - - deprecated: true - version: 1.30.4 - - deprecated: true - version: 1.30.3 - - deprecated: true - version: 1.30.2 - - deprecated: true - version: 1.30.1 -``` - -You can also see the onboarding cluster that has been created by the openmcp-operator. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get clusters.clusters.openmcp.cloud -A -``` - -You should see output similar to the following: - -```shell -NAMESPACE NAME PURPOSES PHASE VERSION PROVIDER AGE -openmcp-system onboarding ["onboarding"] Ready 1.32.7 gardener 30m -``` - -You can also get the shoot name of the onboarding cluster: - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get clusters.clusters.openmcp.cloud --namespace openmcp-system onboarding -o jsonpath="{.status.providerStatus.shoot.metadata.name}" -``` - -You should see output similar to the following: - -```shell -s-hl4uutd4 -``` - -If you want, you can inspect the Gardener shoot in your Gardener project. - -### Get Access to the Onboarding Cluster - -In order to create resources on the onboarding cluster, you need to get access to the onboarding cluster. -To do so, create an access request that grants admin permissions on the onboarding cluster. - -Create a file named `onboarding-access-request.yaml` in the configuration folder with the following content: - -```yam title="config/onboarding-access-request.yaml" -apiVersion: clusters.openmcp.cloud/v1alpha1 -kind: AccessRequest -metadata: - name: bootstrapper-onboarding - namespace: openmcp-system -spec: - clusterRef: - name: onboarding - namespace: openmcp-system - token: - permissions: - - rules: - - apiGroups: - - '*' - resources: - - '*' - verbs: - - '*' -``` - -Then apply the file to the platform cluster: - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig apply -f ./config/onboarding-access-request.yaml -``` - -You can check the status of the access request using the following command: - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get accessrequests.clusters.openmcp.cloud --namespace openmcp-system bootstrapper-onboarding -``` - -Once the access request has been granted, you should see output similar to the following: - -```shell -NAME PHASE -bootstrapper-onboarding Granted -``` - -Now you can get the kubeconfig of the onboarding cluster using the following command: - -```shell -SECRET_NAME=$(kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get accessrequests.clusters.openmcp.cloud --namespace openmcp-system bootstrapper-onboarding -o jsonpath="{.status.secretRef.name}") -SECRET_NAMESPACE=$(kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get accessrequests.clusters.openmcp.cloud --namespace openmcp-system bootstrapper-onboarding -o jsonpath="{.status.secretRef.namespace}") -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get secret ${SECRET_NAME} -n ${SECRET_NAMESPACE} -o jsonpath="{.data.kubeconfig}" | base64 -d > ./kubeconfigs/onboarding.kubeconfig -``` - -### Create a Managed Control Plane on the Onboarding Cluster - -Create a file named `my-mcp.yaml` with the following content in the configuration folder: - -```yaml title="config/my-mcp.yaml" -apiVersion: core.openmcp.cloud/v2alpha1 -kind: ManagedControlPlaneV2 -metadata: - name: my-mcp - namespace: default -spec: - iam: - tokens: - - name: admin - roleRefs: - - kind: ClusterRole - name: cluster-admin -``` - -Apply the file to the onboarding cluster: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig apply -f ./config/my-mcp.yaml -``` - -The openmcp-operator should start to create the necessary resources in order to create the managed control plane. As a result, a new `Managed Control Plane` should be available soon. -You can check the status of the Managed Control Plane using the following command: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get managedcontrolplanev2 -n default my-mcp -o yaml -``` - -After some time (this can take about 10 to 15 minutes), you should see output similar to the following: - -```yaml -apiVersion: core.openmcp.cloud/v2alpha1 -kind: ManagedControlPlaneV2 -metadata: - annotations: - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"core.openmcp.cloud/v2alpha1","kind":"ManagedControlPlaneV2","metadata":{"annotations":{},"name":"my-mcp","namespace":"default"},"spec":{"iam":{"tokens":[{"name":"admin","roleRefs":[{"kind":"ClusterRole","name":"cluster-admin"}]}]}}} - creationTimestamp: "2025-10-01T11:02:29Z" - finalizers: - - core.openmcp.cloud/mcp - - request.clusters.openmcp.cloud/my-mcp - generation: 1 - name: my-mcp - namespace: default - resourceVersion: "32021" - uid: acd0ce65-df78-4667-8b9c-540843a43294 -spec: - iam: - tokens: - - name: admin - roleRefs: - - kind: ClusterRole - name: cluster-admin -status: - access: - token_admin: - name: zmr7k5u7 - conditions: - - lastTransitionTime: "2025-10-01T11:06:35Z" - message: "" - observedGeneration: 1 - reason: AccessReady:token_admin_True - status: "True" - type: AccessReady.token_admin - - lastTransitionTime: "2025-10-01T11:06:35Z" - message: All accesses are ready - observedGeneration: 1 - reason: AllAccessReady_True - status: "True" - type: AllAccessReady - - lastTransitionTime: "2025-10-01T11:02:34Z" - message: "" - observedGeneration: 1 - reason: ClusterConfigurations_True - status: "True" - type: Cluster.ClusterConfigurations - - lastTransitionTime: "2025-10-01T11:06:35Z" - message: API server /healthz endpoint responded with success status code. - observedGeneration: 1 - reason: HealthzRequestSucceeded - status: "True" - type: Cluster.Gardener_APIServerAvailable - - lastTransitionTime: "2025-10-01T11:21:55Z" - message: All control plane components are healthy. - observedGeneration: 1 - reason: ControlPlaneRunning - status: "True" - type: Cluster.Gardener_ControlPlaneHealthy - - lastTransitionTime: "2025-10-01T11:21:55Z" - message: All nodes are ready. - observedGeneration: 1 - reason: EveryNodeReady - status: "True" - type: Cluster.Gardener_EveryNodeReady - - lastTransitionTime: "2025-10-01T11:21:55Z" - message: All observability components are healthy. - observedGeneration: 1 - reason: ObservabilityComponentsRunning - status: "True" - type: Cluster.Gardener_ObservabilityComponentsHealthy - - lastTransitionTime: "2025-10-01T11:21:55Z" - message: All system components are healthy. - observedGeneration: 1 - reason: SystemComponentsRunning - status: "True" - type: Cluster.Gardener_SystemComponentsHealthy - - lastTransitionTime: "2025-10-01T11:02:34Z" - message: "" - observedGeneration: 1 - reason: LandscapeManagement_True - status: "True" - type: Cluster.LandscapeManagement - - lastTransitionTime: "2025-10-01T11:02:34Z" - message: "" - observedGeneration: 1 - reason: Meta_True - status: "True" - type: Cluster.Meta - - lastTransitionTime: "2025-10-01T11:02:34Z" - message: "" - observedGeneration: 1 - reason: ShootManagement_True - status: "True" - type: Cluster.ShootManagement - - lastTransitionTime: "2025-10-01T11:02:34Z" - message: Cluster conditions have been synced to MCP - observedGeneration: 1 - reason: ClusterConditionsSynced_True - status: "True" - type: ClusterConditionsSynced - - lastTransitionTime: "2025-10-01T11:02:34Z" - message: ClusterRequest is ready - observedGeneration: 1 - reason: ClusterRequestReady_True - status: "True" - type: ClusterRequestReady - - lastTransitionTime: "2025-10-01T11:02:29Z" - message: "" - observedGeneration: 1 - reason: Meta_True - status: "True" - type: Meta - observedGeneration: 1 - phase: Ready -``` - -The `status.phase` should be `Ready` and the `AllAccessReady` condition should be `True`. - -You can now get the kubeconfig of the managed control plane using the following command: - -```shell -TOKEN_NAME=$(kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get managedcontrolplanev2 -n default my-mcp -o jsonpath="{.status.access.token_admin.name}") -TOKEN_NAMESPACE=$(kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get managedcontrolplanev2 -n default my-mcp -o jsonpath="{.metadata.namespace}") -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get secret ${TOKEN_NAME} -n ${TOKEN_NAMESPACE} -o jsonpath="{.data.kubeconfig}" | base64 -d > ./kubeconfigs/my-mcp.kubeconfig -``` - -### Deploy the Crossplane Service Provider on the platform cluster - -Update the bootstrapping configuration file (bootstrapper-config.yaml) to include the crossplane service provider. - -```yaml title="config/bootstrapper-config.yaml" -component: - location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: - -repository: - url: https://github.com// - pushBranch: - -environment: - -providers: - clusterProviders: - - name: gardener - serviceProviders: - - name: crossplane - -openmcpOperator: - config: - managedControlPlane: - mcpClusterPurpose: mcp-worker - reconcileMCPEveryXDays: 7 - scheduler: - scope: Cluster - purposeMappings: - mcp-worker: - template: - metadata: - namespace: openmcp-system - spec: - profile: .gardener.shoot-small - tenancy: Exclusive - platform: - template: - metadata: - namespace: openmcp-system - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: .gardener.shoot-small - tenancy: Shared - onboarding: - template: - metadata: - namespace: openmcp-system - labels: - clusters.openmcp.cloud/delete-without-requests: "false" - spec: - profile: .gardener.shoot-workerless - tenancy: Shared - workload: - tenancyCount: 20 - template: - metadata: - namespace: openmcp-system - spec: - profile: .gardener.shoot-small - tenancy: Shared -``` - -Then create a file named `crossplane-provider.yaml` with the following content, and save it in the new `extra-manifests` folder. - -:::info -Note that service provider crossplane only supports the installation of crossplane from an OCI registry. Replace the chart locations in the `ProviderConfig` with the OCI registry where you mirror your crossplane chart versions. OpenMCP will provide this as part of an open source [Releasechannel](https://github.com/openmcp-project/backlog/issues/323) in an upcoming update. -::: - -```yaml title="config/extra-manifests/crossplane-provider.yaml" -apiVersion: crossplane.services.openmcp.cloud/v1alpha1 -kind: ProviderConfig -metadata: - name: default -spec: - versions: - - version: v2.0.2 - chart: - url: ghcr.io/openmcp-project/charts/crossplane:2.0.2 - image: - url: xpkg.crossplane.io/crossplane/crossplane:v2.0.2 - - version: v1.20.1 - chart: - url: ghcr.io/openmcp-project/charts/crossplane:1.20.1 - image: - url: xpkg.crossplane.io/crossplane/crossplane:v1.20.1 - providers: - availableProviders: - - name: provider-kubernetes - package: xpkg.upbound.io/upbound/provider-kubernetes - versions: - - v0.16.0 -``` - -Run the `openmcp-bootstrapper` CLI tool to update the Git repository and deploy the crossplane service provider to the Shoot cluster. - -```shell -docker run --rm -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} manage-deployment-repo --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform.kubeconfig --extra-manifest-dir /config/extra-manifests /config/bootstrapper-config.yaml -``` - -See the `--extra-manifest-dir` parameter that points to the folder containing the extra manifest file created in the previous step. All manifest files in this folder will be added to the Kustomization used by FluxCD to deploy openMCP to the Shoot cluster. - -The git repository should now be updated: - -```shell -. -├── envs -│   └── dev -│   ├── fluxcd -│   │   ├── flux-kustomization.yaml -│   │   ├── gitrepo.yaml -│   │   └── kustomization.yaml -│   ├── kustomization.yaml -│   ├── openmcp -│   │   ├── config -│   │   │   └── openmcp-operator-config.yaml -│   │   └── kustomization.yaml -│   └── root-kustomization.yaml -└── resources - ├── fluxcd - │   ├── components.yaml - │   ├── flux-kustomization.yaml - │   ├── gitrepo.yaml - │   └── kustomization.yaml - ├── kustomization.yaml - ├── openmcp - │   ├── cluster-providers - │   │   └── gardener.yaml - │   ├── crds - │   │   ├── clusters.openmcp.cloud_accessrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusterprofiles.yaml - │   │   ├── clusters.openmcp.cloud_clusterrequests.yaml - │   │   ├── clusters.openmcp.cloud_clusters.yaml - │   │   ├── crossplane.services.openmcp.cloud_providerconfigs.yaml - │   │   ├── gardener.clusters.openmcp.cloud_clusterconfigs.yaml - │   │   ├── gardener.clusters.openmcp.cloud_landscapes.yaml - │   │   ├── gardener.clusters.openmcp.cloud_providerconfigs.yaml - │   │   ├── openmcp.cloud_clusterproviders.yaml - │   │   ├── openmcp.cloud_platformservices.yaml - │   │   └── openmcp.cloud_serviceproviders.yaml - │   ├── deployment.yaml - │   ├── extra - │   │   ├── crossplane-provider.yaml - │   │   ├── gardener-cluster-provider-shoot-small.yaml - │   │   ├── gardener-cluster-provider-shoot-workerless.yaml - │   │   └── gardener-landscape.yaml - │   ├── kustomization.yaml - │   ├── namespace.yaml - │   ├── rbac.yaml - │   └── service-providers - │   └── crossplane.yaml - └── root-kustomization.yaml -``` - -After a while, the Kustomization in the platform cluster should be updated and the crossplane service provider should be deployed: -You can force an update of the Kustomization in the platform cluster to pick up the changes made in the Git repository. - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system annotate gitrepository environments reconcile.fluxcd.io/requestedAt="$(date +%s)" -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n default patch kustomization bootstrap --type merge -p '{"spec":{"force":true}}' -``` - -List the pods in the `openmcp-system` namespace again: - -```shell -kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n openmcp-system -```` - -You should see output similar to the following: - -```shell -NAME READY STATUS RESTARTS AGE -cp-gardener-84b7ff4c9c-vf2sc 1/1 Running 0 3m3s -cp-gardener-init-xr7fs 0/1 Completed 0 3m7s -openmcp-operator-785b967f66-h2dlh 1/1 Running 0 74m -ps-managedcontrolplane-5b77749f7b-mtffp 1/1 Running 0 71m -ps-managedcontrolplane-init-pklrl 0/1 Completed 0 74m -``` - -You should see that the crossplane service provider is running. This means that from now on, the openMCP is able to provide Crossplane service instances, using the new service provider Crossplane. - -### Create a Crossplane service instance on the onboarding cluster - -Create a file named `crossplane-instance.yaml` with the following content in the configuration folder: - -```yaml title="config/crossplane-instance.yaml" -apiVersion: crossplane.services.openmcp.cloud/v1alpha1 -kind: Crossplane -metadata: - name: my-mcp - namespace: default -spec: - version: v1.20.0 - providers: - - name: provider-kubernetes - version: v0.16.0 -``` - -Apply the file to onboarding cluster: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig apply -f ./config/crossplane-instance.yaml -``` - -The Crossplane service provider should now start to create the necessary resources for the new Crossplane instance. As a result, a new Crossplane service instance should soon be available. -You can check the status of the Crossplane instance using the following command: - -```shell -kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get crossplane -n default my-mcp -o yaml -``` - -After a while, you should see output similar to the following: - -```yaml -apiVersion: crossplane.services.openmcp.cloud/v1alpha1 -kind: Crossplane -metadata: - finalizers: - - openmcp.cloud/finalizers - generation: 1 - name: sample - namespace: default -spec: - providers: - - name: provider-kubernetes - version: v0.16.0 - version: v1.20.0 -status: - conditions: - - lastTransitionTime: "2025-09-16T14:09:56Z" - message: Crossplane is healthy. - reason: Healthy - status: "True" - type: CrossplaneReady - - lastTransitionTime: "2025-09-16T14:10:01Z" - message: ProviderKubernetes is healthy. - reason: Healthy - status: "True" - type: ProviderKubernetesReady - observedGeneration: 0 - phase: "" -``` - -Crossplane and the provider Kubernetes should now be available on the mcp cluster. - -```shell -kubectl --kubeconfig ./kubeconfigs/my-mcp.kubeconfig api-resources | grep 'crossplane\|kubernetes' -``` diff --git a/docs/operators/01-setup.md b/docs/operators/01-setup.md new file mode 100644 index 0000000..e0409c3 --- /dev/null +++ b/docs/operators/01-setup.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 0 +--- + +import Tabs from '@theme/Tabs'; +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; + +# Setup + +## Prerequisites + +* A target Kubernetes cluster that matches the desired cluster provider being used (e.g. `Kind` for local testing, `Gardener` for Gardener Shoots) +* A Git repository that will be used to store the desired state of the openMCP landscape +* An OCI registry that contains the `openMCP Root OCM Component` (e.g. `ghcr.io/openmcp-project`) + +:::info +The Git repository used in the following examples must exist before running the `openmcp-bootstrapper` CLI tool. The `openmcp-bootstrapper` is using the default branch (like `main`) as a source to create the desired branch. +The default branch may not be empty, but it should not contain any files or folders that would conflict with the files and folders created by the `openmcp-bootstrapper`. A recommendation is to create an empty repository with a `README.md` file. +::: + +## Download the `openmcp-bootstrapper` CLI tool + +The `openmcp-bootstrapper` CLI tool can be downloaded as an OCI image from an OCI registry (e.g. `ghcr.io/openmcp-project`). +In this example docker will be used to run the `openmcp-bootstrapper` CLI tool. If you don't use docker, adjust the command accordingly. + +Retrieve the latest version of the `openmcp-bootstrapper`: + +```shell +TAG=$(curl -s "https://api.github.com/repos/openmcp-project/bootstrapper/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) +export OPENMCP_BOOTSTRAPPER_VERSION="${TAG}" +``` + +Pull the latest version of the `openmcp-bootstrapper`: + +```shell +docker pull ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} +``` + +## Next Steps + +Choose your cluster provider to continue: +- [Kind Provider](./kind-provider) - For local testing and development +- [Gardener Provider](./gardener-provider) - For production Gardener-based landscapes diff --git a/docs/operators/02-gardener-provider.md b/docs/operators/02-gardener-provider.md new file mode 100644 index 0000000..c3fd368 --- /dev/null +++ b/docs/operators/02-gardener-provider.md @@ -0,0 +1,600 @@ +--- +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs'; +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; + +# Prod: Run on Gardener + +### Requirements + +* A running Gardener installation (see the [Gardener documentation](https://gardener.cloud/docs/) for more information on Gardener) +* A Gardener project in which the clusters will be created +* An infrastructure secret in the Gardener project (see the [Gardener documentation](https://gardener.cloud/docs/getting-started/project/#infrastructure-secrets) for more information on how to create an infrastructure secret) +* Kubectl (see the [Kubectl installation guide](https://kubernetes.io/docs/tasks/tools/#kubectl) for more information on how to install kubectl) +* If the Gardener installation is using OIDC for authentication, install the [OIDC kubectl plugin](https://github.com/int128/kubelogin) +* Good understanding of Gardener and how to create Gardener Shoot clusters and Service Accounts in Gardener Projects. + +### Create a configuration folder + +Create a directory that will be used to store the configuration files and the kubeconfig files. +To keep this example simple, we will use a single directory named `config` in the current working directory. + +```shell +mkdir config +``` + +All following examples will use the `config` directory as the configuration directory. If you use a different directory, replace all occurrences of `config` with your desired directory path. + +Create a directory named `kubeconfigs` in the configuration folder to store the kubeconfig files of the created clusters. + +```shell +mkdir kubeconfigs +``` + +### Create a Gardener Shoot for the Platform Cluster + +openMCP requires a running Kubernetes cluster that acts as the platform cluster. +The platform cluster hosts the openmcp-operator and all service providers, cluster providers and platform services. +In this example, we will create a Gardener Shoot cluster that acts as the platform cluster. See the [Gardener documentation](https://gardener.cloud/docs/getting-started/shoots/) for more information on how to create a Gardener Shoot cluster. + +Create a script folder named `scripts`: + +```shell +mkdir scripts +``` + +Create a file named `get-shoot-kubeconfig.sh` in the `scripts` folder with the following content: + +```shell title="scripts/get-shoot-kubeconfig.sh" +#!/usr/bin/env bash + +GARDENER_SECRET=$1 +NAMESPACE="garden-$2" +SHOOT_NAME=$3 + +REQUEST_PATH="$(mktemp -d)" +REQUEST="${REQUEST_PATH}/admin-kubeconfig-request.json" + +echo "{ \"apiVersion\": \"authentication.gardener.cloud/v1alpha1\", \"kind\": \"AdminKubeconfigRequest\", \"spec\": { \"expirationSeconds\": 7776000 } }" > ${REQUEST} 2>/dev/null + +KUBECONFIG=$(kubectl --kubeconfig "${GARDENER_SECRET}" create \ + -f ${REQUEST} \ + --raw /apis/core.gardener.cloud/v1beta1/namespaces/${NAMESPACE}/shoots/${SHOOT_NAME}/adminkubeconfig 2>/dev/null | jq -r ".status.kubeconfig" | base64 -d) + + +echo "${KUBECONFIG}" +``` + +Make the script executable: + +```shell +chmod +x ./scripts/get-shoot-kubeconfig.sh +``` + +In order to execute this script, you need a kubeconfig file that has access to the Gardener installation. This can be aquired by navigating to the Gardener dashboard, then selecting your user (icon in the upper right corner) -> click 'My Account' and under `Access` download the Kubeconfig file. + +Alternatively, you can create a service account with the `Admin` role in the Gardener project and then retrieve the kubeconfig for the service account. See the [Gardener documentation](https://gardener.cloud/docs/getting-started/project/#service-accounts) for more information on how to create a service account. + +Now, create a new Gardener Shoot cluster in your Gardener project using the Gardener dashboard or the Gardener API via kubectl. The name of the Shoot cluster shall be `platform`. +Please consult the [Gardener documentation](https://gardener.cloud/docs/getting-started/shoots/) for more information on how to create a Gardener Shoot cluster. + +Download the admin kubeconfig of the `platform` Shoot cluster using the script created above (`get-shoot-kubeconfig.sh`) and save it to a file named `platform.kubeconfig` in the `kubeconfigs` folder. + +```shell +./scripts/get-shoot-kubeconfig.sh platform > ./kubeconfigs/platform.kubeconfig +``` + +### Create a bootstrapping configuration file (bootstrapper-config.yaml) in the configuration folder + +Replace `` and `` with your Git organization and repository name. +The environment can be set to the logical environment name (e.g. `dev`, `prod`, `live-eu-west`) that will be used in the Git repository to separate different environments. +The branch can be set to the desired branch name in the Git repository that will be used to store the desired state of the openMCP landscape. + +Get the latest version of the `github.com/openmcp/openmcp` root component: + +```shell +TAG=$(curl -s "https://api.github.com/repos/openmcp-project/openmcp/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) +echo "${TAG}" +``` + +In the bootstrapper configuration, replace `` with the latest version of the `github.com/openmcp-project/openmcp` root component: + +```yaml title="config/bootstrapper-config.yaml" +component: + location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: + +repository: + url: https://github.com// + pushBranch: + +environment: + +openmcpOperator: + config: {} +``` + +### Create a Git configuration file (git-config.yaml) in the configuration folder + +For GitHub use a personal access token with `repo` write permissions. +It is also possible to use a fine-grained token. In this case, it requires read and write permissions for `Contents`. + +```yaml title="config/git-config.yaml" +auth: + basic: + username: "" + password: "" +``` + +### Run the `openmcp-bootstrapper` CLI tool to deploy FluxCD to the Platform Cluster + +Run the `openmcp-bootstrapper` CLI tool to deploy FluxCD to the `platform` Gardener Shoot cluster: + +```shell +docker run --rm -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} deploy-flux --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform.kubeconfig /config/bootstrapper-config.yaml +``` + +You should see output similar to the following: + +```shell +Info: Starting deployment of Flux controllers with config file: /config/bootstrapper-config.yaml. +Info: Ensure namespace flux-system exists +Info: Creating/updating git credentials secret flux-system/git +Info: Created/updated git credentials secret flux-system/git +Info: Creating working directory for gitops-templates +Info: Downloading templates +/tmp/openmcp.cloud.bootstrapper-3041773446/download: 9 file(s) with 691073 byte(s) written +Info: Arranging template files +Info: Arranged template files +Info: Applying templates from gitops-templates/fluxcd to deployment repository +Info: Kustomizing files in directory: /tmp/openmcp.cloud.bootstrapper-3041773446/repo/envs/dev/fluxcd +Info: Applying flux deployment objects +Info: Deployment of flux controllers completed +``` + +### Inspect the deployed FluxCD controllers and Kustomization + +Load the kubeconfig of the Kind cluster and check the deployed FluxCD controllers and the created GitRepository and Kustomization. + +```shell +kind get kubeconfig --name platform > ./kubeconfigs/platform.kubeconfig +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n flux-system +``` + +You should see output similar to the following: + +```shell +NAME READY STATUS RESTARTS AGE +helm-controller-648cdbf8d8-8jhnf 1/1 Running 0 9m37s +image-automation-controller-56df4c78dc-qwmfm 1/1 Running 0 9m35s +image-reflector-controller-56f69fcdc9-pgcgx 1/1 Running 0 9m35s +kustomize-controller-b4c4dcdc8-g49gc 1/1 Running 0 9m38s +notification-controller-59d754d599-w7fjp 1/1 Running 0 9m36s +source-controller-6b45b6464f-jbgb6 1/1 Running 0 9m38s +``` + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A +```` + +You should see output similar to the following: + +```shell +NAMESPACE NAME URL AGE READY STATUS +flux-system environments https://github.com// 86s False failed to checkout and determine revision: unable to clone 'https://github.com//': couldn't find remote ref "refs/heads/" +``` + +This error is expected as the branch does not exist yet in the Git repository. The `openmcp-bootstrapper` will create the branch in the next step. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A +``` + +You should see output similar to the following: + +```shell +NAMESPACE NAME AGE READY STATUS +flux-system flux-system 3m15s False Source artifact not found, retrying in 30s +``` + +This error is also expected as the GitRepository does not exist yet. The `openmcp-bootstrapper` will create the GitRepository in the next step. + +### Run the `openmcp-bootstrapper` CLI tool to deploy openMCP to the Kind cluster + +Update the bootstrapping configuration file (bootstrapper-config.yaml) to include the Gardener cluster provider and the openmcp-operator configuration. + +Please replace `` with the logical environment name (e.g. `dev`, `prod`, `live-eu-west`) that will be used in the Git repository to separate different environments. Notice that the same environment name must be used in the `environment` field and in the scheduler profiles. + +```yaml title="config/bootstrapper-config.yaml" +component: + location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: + +repository: + url: https://github.com// + pushBranch: + +environment: + +providers: + clusterProviders: + - name: gardener + +openmcpOperator: + config: + managedControlPlane: + mcpClusterPurpose: mcp-worker + reconcileMCPEveryXDays: 7 + scheduler: + scope: Cluster + purposeMappings: + mcp-worker: + template: + metadata: + namespace: openmcp-system + spec: + profile: .gardener.shoot-small + tenancy: Exclusive + platform: + template: + metadata: + namespace: openmcp-system + labels: + clusters.openmcp.cloud/delete-without-requests: "false" + spec: + profile: .gardener.shoot-small + tenancy: Shared + onboarding: + template: + metadata: + namespace: openmcp-system + labels: + clusters.openmcp.cloud/delete-without-requests: "false" + spec: + profile: .gardener.shoot-workerless + tenancy: Shared + workload: + tenancyCount: 20 + template: + metadata: + namespace: openmcp-system + spec: + profile: .gardener.shoot-small + tenancy: Shared +``` + +Create a directory named `extra-manifests` in the configuration folder. + +```shell +mkdir ./config/extra-manifests +``` + +In the `extra-manifests` folder, create a file named `gardener-landscape.yaml` with the following content: + +```yaml title="config/extra-manifests/gardener-landscape.yaml" +apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 +kind: Landscape +metadata: + name: gardener-landscape +spec: + access: + secretRef: + name: gardener-landscape-kubeconfig + namespace: openmcp-system +``` + +The gardener landscape configuration requires a secret that contains the kubeconfig to access the Gardener project. For that purpose, create a secret named `gardener-landscape-kubeconfig` in the `openmcp-system` namespace of the platform cluster that contains the kubeconfig file that has access to the Gardener installation. +See the [Gardener documentation](https://gardener.cloud/docs/dashboard/automated-resource-management/#create-a-service-account) on how to create a service account in the Gardener project using the Gardener dashboard. +Create a service account with at least the `admin` role in the Gardener project. Then [download](https://gardener.cloud/docs/dashboard/automated-resource-management/#use-the-service-account) the kubeconfig for the service account and save it to a file named `./kubeconfigs/gardener-landscape.kubeconfig`. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig create namespace openmcp-system +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig create secret generic gardener-landscape-kubeconfig --from-file=kubeconfig=./kubeconfigs/gardener-landscape.kubeconfig -n openmcp-system +``` + +The kubeconfig content can be retrieved from the Gardener dashboard or by creating a service account in the Gardener project. See the [Gardener documentation](https://gardener.cloud/docs/getting-started/project/#service-accounts) for more information on how to create a service account. +The service account requires at least the `admin` role in the Gardener project. + +In the `extra-manifests` folder, create a file named `gardener-cluster-provider-shoot-small.yaml` with the following content: + + + + +```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-small.yaml" +apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 +kind: ProviderConfig +metadata: + name: shoot-small +spec: + landscapeRef: + name: gardener-landscape + project: + providerRef: + name: gardener + shootTemplate: + spec: + cloudProfile: + kind: CloudProfile + name: gcp + kubernetes: + version: "" # e.g. "1.32" + maintenance: + autoUpdate: + kubernetesVersion: true + timeWindow: + begin: 220000+0200 + end: 230000+0200 + networking: + nodes: 10.180.0.0/16 + type: calico + provider: + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: # e.g. europe-west1-c + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + type: gcp + workers: + - cri: + name: containerd + machine: + architecture: amd64 + image: + name: gardenlinux + version: "" # e.g. "1592.9.0" + type: n1-standard-2 + maxSurge: 1 + maximum: 5 + minimum: 1 + name: default-worker + volume: + size: 50Gi + type: pd-balanced + zones: + - # e.g. europe-west1-c + purpose: evaluation + region: # e.g. europe-west1 + secretBindingName: +``` + + + +```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-small.yaml" +apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 +kind: ProviderConfig +metadata: + name: shoot-small +spec: + landscapeRef: + name: gardener-landscape + project: + providerRef: + name: gardener + shootTemplate: + spec: + cloudProfile: + kind: CloudProfile + name: aws + kubernetes: + version: "" # e.g. "1.32" + maintenance: + autoUpdate: + kubernetesVersion: true + timeWindow: + begin: 220000+0200 + end: 230000+0200 + networking: + type: calico + nodes: 10.180.0.0/16 + provider: + controlPlaneConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + cloudControllerManager: + useCustomRouteController: true + storage: + managedDefaultClass: true + infrastructureConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + vpc: + cidr: 10.180.0.0/16 + zones: + - name: # e.g. eu-west-1a + workers: 10.180.0.0/19 + public: 10.180.32.0/20 + internal: 10.180.48.0/20 + type: aws + workers: + - cri: + name: containerd + machine: + architecture: amd64 + image: + name: gardenlinux + version: "" # e.g. "1592.9.0" + type: m5.large + maxSurge: 1 + maximum: 5 + minimum: 1 + name: default-worker + volume: + size: 50Gi + type: gp3 + zones: + - # e.g. eu-west-1a + purpose: evaluation + region: # e.g. eu-west-1 + secretBindingName: +``` + + + + +In the `extra-manifests` folder, create a file named `gardener-cluster-provider-shoot-workerless.yaml` with the following content: + + + + +```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-workerless.yaml" +apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 +kind: ProviderConfig +metadata: + name: shoot-workerless +spec: + landscapeRef: + name: gardener-landscape + project: + providerRef: + name: gardener + shootTemplate: + spec: + cloudProfile: + kind: CloudProfile + name: gcp + kubernetes: + version: "" # e.g. "1.32" + maintenance: + autoUpdate: + kubernetesVersion: true + timeWindow: + begin: 220000+0200 + end: 230000+0200 + provider: + type: gcp + purpose: evaluation + region: # eg europe-west1 +``` + + + +```yaml title="config/extra-manifests/gardener-cluster-provider-shoot-workerless.yaml" +apiVersion: gardener.clusters.openmcp.cloud/v1alpha1 +kind: ProviderConfig +metadata: + name: shoot-workerless +spec: + landscapeRef: + name: gardener-landscape + project: + providerRef: + name: gardener + shootTemplate: + spec: + cloudProfile: + kind: CloudProfile + name: aws + kubernetes: + version: "" # e.g. "1.32" + maintenance: + autoUpdate: + kubernetesVersion: true + timeWindow: + begin: 220000+0200 + end: 230000+0200 + provider: + type: aws + purpose: evaluation + region: # e.g. eu-west-1 +``` + + + + +Replace `` with the name of your Gardener project and `` with the name of the secret binding that contains the infrastructure secret for your Gardener project. + +Replace also `` with the desired Kubernetes version (e.g. `1.32`), `` with the desired Garden Linux version (e.g. `1592.9.0`), `` with the desired region (e.g. `europe-west1`), and `` with the desired zone (e.g. `europe-west1-c`). + +:::info +Please adjust the shoot configuration based on your specific needs, e.g. change `Evaluation` to `Production` as purpose, if you are planning to use the MCP for productive purposes. For all the details reg. Shoot configuration, please consult the respective Gardener documentation. +::: + +Now run the `openmcp-bootstrapper` CLI tool to update the Git repository and deploy openMCP to the `platform` Gardener Shoot cluster: + +```shell +docker run --rm -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} manage-deployment-repo --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform.kubeconfig --extra-manifest-dir /config/extra-manifests /config/bootstrapper-config.yaml +``` + +You should see output similar to the following: + +```shell +Info: Downloading component ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.25 +Info: Creating template transformer +Info: Downloading template resources +/tmp/openmcp.cloud.bootstrapper-245193548/transformer/download/fluxcd: 9 file(s) with 691073 byte(s) written +/tmp/openmcp.cloud.bootstrapper-245193548/transformer/download/openmcp: 8 file(s) with 6625 byte(s) written +Info: Transforming templates into deployment repository structure +Info: Fetching openmcp-operator component version +Info: Cloning deployment repository https://github.com/reshnm/openmcp-deployment +Info: Checking out or creating branch gardener +Info: Applying templates from "gitops-templates/fluxcd"/"gitops-templates/openmcp" to deployment repository +Info: Templating providers: clusterProviders=[{gardener [] map[]}], serviceProviders=[], platformServices=[], imagePullSecrets=[] +Info: Applying Custom Resource Definitions to deployment repository +/tmp/openmcp.cloud.bootstrapper-245193548/repo/resources/openmcp/crds: 8 file(s) with 484832 byte(s) written +/tmp/openmcp.cloud.bootstrapper-245193548/repo/resources/openmcp/crds: 3 file(s) with 198428 byte(s) written +Info: Applying extra manifests from /config/extra-manifests to deployment repository +Info: Committing and pushing changes to deployment repository +Info: Created commit: ee2b6ef079808fbc198b4f6eced1afb89f64d1d1 +Info: Running kustomize on /tmp/openmcp.cloud.bootstrapper-245193548/repo/envs/dev +Info: Applying Kustomization manifest: default/bootstrap +``` + +### Inspect the Git repository + +The desired state of the openMCP landscape has now been created in the Git repository and should look similar to the following structure: + +```shell +. +├── envs +│   └── dev +│   ├── fluxcd +│   │   ├── flux-kustomization.yaml +│   │   ├── gitrepo.yaml +│   │   └── kustomization.yaml +│   ├── kustomization.yaml +│   ├── openmcp +│   │   ├── config +│   │   │   └── openmcp-operator-config.yaml +│   │   └── kustomization.yaml +│   └── root-kustomization.yaml +└── resources + ├── fluxcd + │   ├── components.yaml + │   ├── flux-kustomization.yaml + │   ├── gitrepo.yaml + │   └── kustomization.yaml + ├── kustomization.yaml + ├── openmcp + │   ├── cluster-providers + │   │   └── gardener.yaml + │   ├── crds + │   │   ├── clusters.openmcp.cloud_accessrequests.yaml + │   │   ├── clusters.openmcp.cloud_clusterprofiles.yaml + │   │   ├── clusters.openmcp.cloud_clusterrequests.yaml + │   │   ├── clusters.openmcp.cloud_clusters.yaml + │   │   ├── gardener.clusters.openmcp.cloud_clusterconfigs.yaml + │   │   ├── gardener.clusters.openmcp.cloud_landscapes.yaml + │   │   ├── gardener.clusters.openmcp.cloud_providerconfigs.yaml + │   │   ├── openmcp.cloud_clusterproviders.yaml + │   │   ├── openmcp.cloud_platformservices.yaml + │   │   └── openmcp.cloud_serviceproviders.yaml + │   ├── deployment.yaml + │   ├── extra + │   │   ├── gardener-cluster-provider-shoot-small.yaml + │   │   ├── gardener-cluster-provider-shoot-workerless.yaml + │   │   └── gardener-landscape.yaml + │   ├── kustomization.yaml + │   ├── namespace.yaml + │   └── rbac.yaml + └── root-kustomization.yaml +``` + +The `envs/` folder contains the Kustomization files that are used by FluxCD to deploy openMCP to the platform cluster. +The `resources` folder contains the base resources that are used by the Kustomization files in the `envs/` folder. + diff --git a/docs/operators/03-kind-provider.md b/docs/operators/03-kind-provider.md new file mode 100644 index 0000000..a87143b --- /dev/null +++ b/docs/operators/03-kind-provider.md @@ -0,0 +1,351 @@ +--- +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; + +# Dev: Run on Kind + +## Requirements + +* [Docker](https://docs.docker.com/get-docker/) installed and running. Docker alternatively can be replaced with another OCI runtime (e.g. Podman) that can run the `openmcp-bootstrapper` CLI tool as an OCI image. +* [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) installed + +:::info +If you are using a docker alternative, make sure that it is correctly setup regarding Docker compatibility. In case of Podman, you should find a corresponding configuration under `Settings` in the Podman UI. +::: + +## Create a configuration folder + +Create a directory that will be used to store the configuration files and the kubeconfig files. +To keep this example simple, we will use a single directory named `config` in the current working directory. + +```shell +mkdir config +``` + +All following examples will use the `config` directory as the configuration directory. If you use a different directory, replace all occurrences of `config` with your desired directory path. + +Create a directory named `kubeconfigs` in the configuration folder to store the kubeconfig files of the created clusters. + +```shell +mkdir kubeconfigs +``` + +## Create the Kind configuration file (kind-config.yaml) in the configuration folder + +```yaml +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +nodes: +- role: control-plane + extraMounts: + - hostPath: /var/run/docker.sock + containerPath: /var/run/host-docker.sock +``` + +## Create the Kind cluster + +Create the Kind cluster using the configuration file created in the previous step. + +:::warning + +Please check if your current `kind` network has a `/16` subnet. This is required for our cluster-provider-kind. +You can check the current network configuration using: + +```shell +docker network inspect kind | jq ".[].IPAM.Config.[].Subnet" +"172.19.0.0/16" +``` + +If the result is not specifying `/16` but something smaller like `/24` you need to delete the network and create a new one. For that **all kind clusters needs to be deleted**. Then run: + +```shell +docker network rm kind + +docker network create kind --subnet 172.19.0.0/16 +``` + +::: + +:::info Podman Support +In case you are using Podman instead of Docker, it is currently required to first create a suitable network for the Kind cluster by executing the following command before creating the Kind cluster itself. + +```shell +podman network create kind --subnet 172.19.0.0/16 +``` + +::: + +```shell +kind create cluster --name platform --config ./config/kind-config.yaml +``` + +Export the internal kubeconfig of the Kind cluster to a file named `platform-int.kubeconfig` in the configuration folder. + +```shell +kind get kubeconfig --internal --name platform > ./kubeconfigs/platform-int.kubeconfig +``` + +## Create a bootstrapping configuration file (bootstrapper-config.yaml) in the configuration folder + +Replace `` and `` with your Git organization and repository name. +The environment can be set to the logical environment name (e.g. `dev`, `prod`, `live-eu-west`) that will be used in the Git repository to separate different environments. +The branch can be set to the desired branch name in the Git repository that will be used to store the desired state of the openMCP landscape. + +Get the latest version of the `github.com/openmcp-project/openmcp` root component: + +```shell +TAG=$(curl -s "https://api.github.com/repos/openmcp-project/openmcp/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) +echo "${TAG}" +``` + +In the bootstrapper configuration, replace `` with the latest version of the `github.com/openmcp/openmcp` root component: + +```yaml title="config/bootstrapper-config.yaml" +component: + location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: + +repository: + url: https://github.com// + pushBranch: + +environment: + +openmcpOperator: + config: {} +``` + +## Create a Git configuration file (git-config.yaml) in the configuration folder + +For GitHub use a personal access token with `repo` write permissions. +It is also possible to use a fine-grained token. In this case, it requires read and write permissions for `Contents`. + +```yaml title="config/git-config.yaml" +auth: + basic: + username: "" + password: "" +``` + +## Run the `openmcp-bootstrapper` CLI tool and deploy FluxCD to the Kind cluster + +```shell +docker run --rm --network kind -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} deploy-flux --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform-int.kubeconfig /config/bootstrapper-config.yaml +``` + +You should see output similar to the following: + +```shell +Info: Starting deployment of Flux controllers with config file: /config/bootstrapper-config.yaml. +Info: Ensure namespace flux-system exists +Info: Creating/updating git credentials secret flux-system/git +Info: Created/updated git credentials secret flux-system/git +Info: Creating working directory for gitops-templates +Info: Downloading templates +/tmp/openmcp.cloud.bootstrapper-3041773446/download: 9 file(s) with 691073 byte(s) written +Info: Arranging template files +Info: Arranged template files +Info: Applying templates from gitops-templates/fluxcd to deployment repository +Info: Kustomizing files in directory: /tmp/openmcp.cloud.bootstrapper-3041773446/repo/envs/dev/fluxcd +Info: Applying flux deployment objects +Info: Deployment of flux controllers completed +``` + +## Inspect the deployed FluxCD controllers and Kustomization + +Load the kubeconfig of the Kind cluster and check the deployed FluxCD controllers and the created GitRepository and Kustomization. + +```shell +kind get kubeconfig --name platform > ./kubeconfigs/platform.kubeconfig +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n flux-system +``` + +You should see output similar to the following: + +```shell +NAME READY STATUS RESTARTS AGE +helm-controller-648cdbf8d8-8jhnf 1/1 Running 0 9m37s +image-automation-controller-56df4c78dc-qwmfm 1/1 Running 0 9m35s +image-reflector-controller-56f69fcdc9-pgcgx 1/1 Running 0 9m35s +kustomize-controller-b4c4dcdc8-g49gc 1/1 Running 0 9m38s +notification-controller-59d754d599-w7fjp 1/1 Running 0 9m36s +source-controller-6b45b6464f-jbgb6 1/1 Running 0 9m38 +``` + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A +```` + +You should see output similar to the following: + +```shell +NAMESPACE NAME URL AGE READY STATUS +flux-system environments https://github.com// 86s False failed to checkout and determine revision: unable to clone 'https://github.com//': couldn't find remote ref "refs/heads/" +``` + +This error is expected as the branch does not exist yet in the Git repository. The `openmcp-bootstrapper` will create the branch in the next step. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A +``` + +You should see output similar to the following: + +```shell +NAMESPACE NAME AGE READY STATUS +flux-system flux-system 3m15s False Source artifact not found, retrying in 30s +``` + +This error is also expected as the GitRepository does not exist yet. The `openmcp-bootstrapper` will create the GitRepository in the next step. + +## Run the `openmcp-bootstrapper` CLI tool to deploy openMCP to the Kind cluster + +Update the bootstrapping configuration file (bootstrapper-config.yaml) to include the kind cluster provider and the openmcp-operator configuration. + +```yaml title="config/bootstrapper-config.yaml" +component: + location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: + +repository: + url: https://github.com// + pushBranch: + +environment: + +providers: + clusterProviders: + - name: kind + config: + extraVolumeMounts: + - mountPath: /var/run/docker.sock + name: docker + extraVolumes: + - name: docker + hostPath: + path: /var/run/host-docker.sock + type: Socket + +openmcpOperator: + config: + managedControlPlane: + mcpClusterPurpose: mcp-worker + reconcileMCPEveryXDays: 7 + scheduler: + scope: Cluster + purposeMappings: + mcp: + template: + spec: + profile: kind + tenancy: Exclusive + mcp-worker: + template: + spec: + profile: kind + tenancy: Exclusive + platform: + template: + metadata: + labels: + clusters.openmcp.cloud/delete-without-requests: "false" + spec: + profile: kind + tenancy: Shared + onboarding: + template: + metadata: + labels: + clusters.openmcp.cloud/delete-without-requests: "false" + spec: + profile: kind + tenancy: Shared + workload: + tenancyCount: 20 + template: + spec: + profile: kind + tenancy: Shared +``` + +```shell +docker run --rm --network kind -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} manage-deployment-repo --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform-int.kubeconfig /config/bootstrapper-config.yaml +``` + +You should see output similar to the following: + +```shell +Info: Downloading component ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.20 +Info: Creating template transformer +Info: Downloading template resources +/tmp/openmcp.cloud.bootstrapper-2402093624/transformer/download/fluxcd: 9 file(s) with 691073 byte(s) written +/tmp/openmcp.cloud.bootstrapper-2402093624/transformer/download/openmcp: 8 file(s) with 6625 byte(s) written +Info: Transforming templates into deployment repository structure +Info: Fetching openmcp-operator component version +Info: Cloning deployment repository https://github.com/reshnm/template-test +Info: Checking out or creating branch kind +Info: Applying templates from "gitops-templates/fluxcd"/"gitops-templates/openmcp" to deployment repository +Info: Templating providers: clusterProviders=[{kind [123 34 101 120 116 114 97 86 111 108 117 109 101 77 111 117 110 116 115 34 58 91 123 34 109 111 117 110 116 80 97 116 104 34 58 34 47 118 97 114 47 114 117 110 47 100 111 99 107 101 114 46 115 111 99 107 34 44 34 110 97 109 101 34 58 34 100 111 99 107 101 114 34 125 93 44 34 101 120 116 114 97 86 111 108 117 109 101 115 34 58 91 123 34 104 111 115 116 80 97 116 104 34 58 123 34 112 97 116 104 34 58 34 47 118 97 114 47 114 117 110 47 104 111 115 116 45 100 111 99 107 101 114 46 115 111 99 107 34 44 34 116 121 112 101 34 58 34 83 111 99 107 101 116 34 125 44 34 110 97 109 101 34 58 34 100 111 99 107 101 114 34 125 93 44 34 118 101 114 98 111 115 105 116 121 34 58 34 100 101 98 117 103 34 125] map[extraVolumeMounts:[map[mountPath:/var/run/docker.sock name:docker]] extraVolumes:[map[hostPath:map[path:/var/run/host-docker.sock type:Socket] name:docker]] verbosity:debug]}], serviceProviders=[], platformServices=[], imagePullSecrets=[] +Info: Applying Custom Resource Definitions to deployment repository +/tmp/openmcp.cloud.bootstrapper-2402093624/repo/resources/openmcp/crds: 8 file(s) with 475468 byte(s) written +/tmp/openmcp.cloud.bootstrapper-2402093624/repo/resources/openmcp/crds: 1 file(s) with 1843 byte(s) written +Info: No extra manifest directory specified, skipping +Info: Committing and pushing changes to deployment repository +Info: Created commit: 287f9e88b905371bba412b5d0286ad02db0f4aac +Info: Running kustomize on /tmp/openmcp.cloud.bootstrapper-2402093624/repo/envs/dev +Info: Applying Kustomization manifest: default/bootstrap + +``` + +## Inspect the Git repository + +The desired state of the openMCP landscape has now been created in the Git repository and should look similar to the following structure: + +```shell +. +├── envs +│ └── dev +│ ├── fluxcd +│ │ ├── flux-kustomization.yaml +│ │ ├── gitrepo.yaml +│ │ └── kustomization.yaml +│ ├── kustomization.yaml +│ ├── openmcp +│ │ ├── config +│ │ │ └── openmcp-operator-config.yaml +│ │ └── kustomization.yaml +│ └── root-kustomization.yaml +└── resources + ├── fluxcd + │ ├── components.yaml + │ ├── flux-kustomization.yaml + │ ├── gitrepo.yaml + │ └── kustomization.yaml + ├── kustomization.yaml + ├── openmcp + │ ├── cluster-providers + │ │ └── kind.yaml + │ ├── crds + │ │ ├── clusters.openmcp.cloud_accessrequests.yaml + │ │ ├── clusters.openmcp.cloud_clusterprofiles.yaml + │ │ ├── clusters.openmcp.cloud_clusterrequests.yaml + │ │ ├── clusters.openmcp.cloud_clusters.yaml + │ │ ├── kind.clusters.openmcp.cloud_providerconfigs.yaml + │ │ ├── openmcp.cloud_clusterproviders.yaml + │ │ ├── openmcp.cloud_platformservices.yaml + │ │ └── openmcp.cloud_serviceproviders.yaml + │ ├── deployment.yaml + │ ├── kustomization.yaml + │ ├── namespace.yaml + │ └── rbac.yaml + └── root-kustomization.yaml +``` + +The `envs/` folder contains the Kustomization files that are used by FluxCD to deploy openMCP to the Kind cluster. +The `resources` folder contains the base resources that are used by the Kustomization files in the `envs/` folder. + +## Next Steps + +Continue to [Verify Setup](./verify-setup) to inspect the Kustomizations and deployed components. diff --git a/docs/operators/04-verify-setup.md b/docs/operators/04-verify-setup.md new file mode 100644 index 0000000..d778295 --- /dev/null +++ b/docs/operators/04-verify-setup.md @@ -0,0 +1,521 @@ +--- +sidebar_position: 3 +--- + +import Tabs from '@theme/Tabs'; +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; + +# Verify Setup + +After deploying OpenMCP using the bootstrapper, verify that all components are running correctly. + + + + +## Inspect the Kustomizations in the Kind cluster + +Force an update of the GitRepository and Kustomization in the Kind cluster to pick up the changes made in the Git repository. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system annotate gitrepository environments reconcile.fluxcd.io/requestedAt="$(date +%s)" +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system patch kustomization flux-system --type merge -p '{"spec":{"force":true}}' +``` + +Get the status of the GitRepository in the Kind cluster. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A +``` + +You should see output similar to the following: + +```shell +NAMESPACE NAME URL AGE READY STATUS +flux-system environments https://github.com// 9m6s True stored artifact for revision 'docs@sha1:...' +``` + +So we have now successfully configured FluxCD to watch for changes in the specified GitHub repository, using the `environments` custom resource of kind `GitRepository`. +Now let's get the status of the Kustomization in the Kind cluster. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A +``` + +You should see output similar to the following: + +```shell +NAMESPACE NAME AGE READY STATUS +default bootstrap 5m31s True Applied revision: docs@sha1:... +flux-system flux-system 10m True Applied revision: docs@sha1:... +``` + +You can see that there are now two Kustomizations in the Kind cluster. +The `flux-system` Kustomization is used to deploy the FluxCD controllers and the `bootstrap` Kustomization is used to deploy openMCP to the Kind cluster. + +## Inspect the deployed openMCP components in the Kind cluster + +Now check the deployed openMCP components. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n openmcp-system +``` + +You should see output similar to the following: + +```shell +NAME READY STATUS RESTARTS AGE +cp-kind-6b4886b7cf-z54pg 1/1 Running 0 20s +cp-kind-init-msqg7 0/1 Completed 0 27s +openmcp-operator-5f784f47d7-nfg65 1/1 Running 0 34s +ps-managedcontrolplane-668c99c97c-9jltx 1/1 Running 0 4s +ps-managedcontrolplane-init-49rx2 0/1 Completed 0 27s +``` + +So now, the openmcp-operator, the managedcontrolplane platform service and the cluster provider kind are running. +You are now ready to create and manage clusters using openMCP. + +## Get Access to the Onboarding Cluster + +The openmcp-operator should now have created a `onboarding Cluster` resource on the platform cluster that represents the onboarding cluster. +The onboarding cluster is a special cluster that is used to create new managed control planes. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get clusters.clusters.openmcp.cloud -A +``` + +You should see output similar to the following: + +```shell +NAMESPACE NAME PURPOSES PHASE VERSION PROVIDER AGE +openmcp-system onboarding ["onboarding"] Ready 11m +``` + +Now you can retrieve the kubeconfig of the onboarding cluster. +Use `kind` to retrieve the list of available clusters. + +```shell +kind get clusters +``` + +You should see output similar to the following: + +```shell +onboarding.12345678 +platform +``` + +You can now see the new onboarding cluster. +Get the kubeconfig of the onboarding cluster and save it to a file named `onboarding.kubeconfig` in the configuration folder. +Please replace `onboarding.12345678` with the actual name of your onboarding cluster. + +```shell +kind get kubeconfig --name onboarding.12345678 > ./kubeconfigs/onboarding.kubeconfig +``` + +## Create a Managed Control Plane + +Create a file named `my-mcp.yaml` with the following content in the configuration folder: + +```yaml title="config/my-mcp.yaml" +apiVersion: core.openmcp.cloud/v2alpha1 +kind: ManagedControlPlaneV2 +metadata: + name: my-mcp + namespace: default +spec: + iam: {} +``` + +Apply the file to the onboarding cluster: + +```shell +kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig apply -f ./config/my-mcp.yaml +``` + +The openmcp-operator should start to create the necessary resources in order to create the managed control plane. As a result, a new `Managed Control Plane` should be available soon. +You can check the status of the Managed Control Plane using the following command: + +```shell +kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get managedcontrolplanev2 -n default my-mcp -o yaml +``` + +You should see output similar to the following: + +```yaml +apiVersion: core.openmcp.cloud/v2alpha1 +kind: ManagedControlPlaneV2 +metadata: + finalizers: + - core.openmcp.cloud/mcp + - request.clusters.openmcp.cloud/sample + name: sample + namespace: default +spec: + iam: {} +status: + conditions: + - lastTransitionTime: "2025-09-16T13:03:55Z" + message: All accesses are ready + observedGeneration: 1 + reason: AllAccessReady_True + status: "True" + type: AllAccessReady + - lastTransitionTime: "2025-09-16T13:03:55Z" + message: Cluster conditions have been synced to MCP + observedGeneration: 1 + reason: ClusterConditionsSynced_True + status: "True" + type: ClusterConditionsSynced + - lastTransitionTime: "2025-09-16T13:03:55Z" + message: ClusterRequest is ready + observedGeneration: 1 + reason: ClusterRequestReady_True + status: "True" + type: ClusterRequestReady + - lastTransitionTime: "2025-09-16T13:03:50Z" + message: "" + observedGeneration: 1 + reason: Meta_True + status: "True" + type: Meta + observedGeneration: 1 + phase: Ready +``` + +You should see that the Managed Control Plane is in phase `Ready`. +The openmcp-operator should now have created a new Kind cluster that represents the Managed Control Plane. +You can check the list of available Kind clusters using the following command: + +```shell +kind get clusters +``` + +You should see output similar to the following: + +```shell +mcp-worker-abcde.87654321 +onboarding.12345678 +platform +``` + +You can now get the kubeconfig of the managed control plane and save it to a file named `my-mcp.kubeconfig` in the kubeconfigs folder. Please replace `mcp-worker-abcde.87654321` with the actual name of your managed control plane cluster. + +```shell +kind get kubeconfig --name mcp-worker-abcde.87654321 > ./kubeconfigs/my-mcp.kubeconfig +``` + +You can now use the kubeconfig to access the Managed Control Plane cluster. + +```shell +kubectl --kubeconfig ./kubeconfigs/my-mcp.kubeconfig get namespaces +``` + +## Deploy the Crossplane Service Provider + +Update the bootstrapping configuration file (bootstrapper-config.yaml) to include the crossplane service provider. + +```yaml title="config/bootstrapper-config.yaml" +component: + location: ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp: + +repository: + url: https://github.com// + pushBranch: + +environment: + +providers: + clusterProviders: + - name: kind + config: + extraVolumeMounts: + - mountPath: /var/run/docker.sock + name: docker + extraVolumes: + - name: docker + hostPath: + path: /var/run/host-docker.sock + type: Socket + serviceProviders: + - name: crossplane + +openmcpOperator: + config: + managedControlPlane: + mcpClusterPurpose: mcp-worker + reconcileMCPEveryXDays: 7 + scheduler: + scope: Cluster + purposeMappings: + mcp: + template: + spec: + profile: kind + tenancy: Exclusive + mcp-worker: + template: + spec: + profile: kind + tenancy: Exclusive + platform: + template: + metadata: + labels: + clusters.openmcp.cloud/delete-without-requests: "false" + spec: + profile: kind + tenancy: Shared + onboarding: + template: + metadata: + labels: + clusters.openmcp.cloud/delete-without-requests: "false" + spec: + profile: kind + tenancy: Shared + workload: + tenancyCount: 20 + template: + spec: + profile: kind + tenancy: Shared +``` + +Create a new folder named `extra-manifests` in the configuration folder. Then create a file named `crossplane-provider.yaml` with the following content, and save it in the new `extra-manifests` folder. + +:::info +Note that service provider crossplane only supports the installation of crossplane from an OCI registry. Replace the chart locations in the `ProviderConfig` with the OCI registry where you mirror your crossplane chart versions. OpenMCP will provide this as part of an open source [Releasechannel](https://github.com/openmcp-project/backlog/issues/323) in an upcoming update. +::: + +```yaml title="config/extra-manifests/crossplane-provider.yaml" +apiVersion: crossplane.services.openmcp.cloud/v1alpha1 +kind: ProviderConfig +metadata: + name: default +spec: + versions: + - version: v2.0.2 + chart: + url: ghcr.io/openmcp-project/charts/crossplane:2.0.2 + image: + url: xpkg.crossplane.io/crossplane/crossplane:v2.0.2 + - version: v1.20.1 + chart: + url: ghcr.io/openmcp-project/charts/crossplane:1.20.1 + image: + url: xpkg.crossplane.io/crossplane/crossplane:v1.20.1 + providers: + availableProviders: + - name: provider-kubernetes + package: xpkg.upbound.io/upbound/provider-kubernetes + versions: + - v0.16.0 +``` + +Run the `openmcp-bootstrapper` CLI tool to update the Git repository and deploy the crossplane service provider to the Kind cluster. + +```shell +docker run --rm --network kind -v ./config:/config -v ./kubeconfigs:/kubeconfigs ghcr.io/openmcp-project/images/openmcp-bootstrapper:${OPENMCP_BOOTSTRAPPER_VERSION} manage-deployment-repo --git-config /config/git-config.yaml --kubeconfig /kubeconfigs/platform-int.kubeconfig --extra-manifest-dir /config/extra-manifests /config/bootstrapper-config.yaml +``` + +See the `--extra-manifest-dir` parameter that points to the folder containing the extra manifest file created in the previous step. All manifest files in this folder will be added to the Kustomization used by FluxCD to deploy openMCP to the Kind cluster. + +The git repository should now be updated: + +```shell +. +├── envs +│ └── dev +│ ├── fluxcd +│ │ ├── flux-kustomization.yaml +│ │ ├── gitrepo.yaml +│ │ └── kustomization.yaml +│ ├── kustomization.yaml +│ ├── openmcp +│ │ ├── config +│ │ │ └── openmcp-operator-config.yaml +│ │ └── kustomization.yaml +│ └── root-kustomization.yaml +└── resources + ├── fluxcd + │ ├── components.yaml + │ ├── flux-kustomization.yaml + │ ├── gitrepo.yaml + │ └── kustomization.yaml + ├── kustomization.yaml + ├── openmcp + │ ├── cluster-providers + │ │ └── kind.yaml + │ ├── crds + │ │ ├── clusters.openmcp.cloud_accessrequests.yaml + │ │ ├── clusters.openmcp.cloud_clusterprofiles.yaml + │ │ ├── clusters.openmcp.cloud_clusterrequests.yaml + │ │ ├── clusters.openmcp.cloud_clusters.yaml + │ │ ├── crossplane.services.openmcp.cloud_providerconfigs.yaml + │ │ ├── kind.clusters.openmcp.cloud_providerconfigs.yaml + │ │ ├── openmcp.cloud_clusterproviders.yaml + │ │ ├── openmcp.cloud_platformservices.yaml + │ │ └── openmcp.cloud_serviceproviders.yaml + │ ├── deployment.yaml + │ ├── extra + │ │ └── crossplane-providers.yaml + │ ├── kustomization.yaml + │ ├── namespace.yaml + │ ├── rbac.yaml + │ └── service-providers + │ └── crossplane.yaml + └── root-kustomization.yaml +``` + +After a while, the Kustomization in the Kind cluster should be updated and the crossplane service provider should be deployed: +You can force an update of the Kustomization in the Kind cluster to pick up the changes made in the Git repository. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system annotate gitrepository environments reconcile.fluxcd.io/requestedAt="$(date +%s)" +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n default patch kustomization bootstrap --type merge -p '{"spec":{"force":true}}' +``` + +List the pods in the `openmcp-system` namespace again: + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n openmcp-system +```` + +You should see output similar to the following: + +```shell +NAME READY STATUS RESTARTS AGE +cp-kind-6b4886b7cf-z54pg 1/1 Running 0 18m +cp-kind-init-msqg7 0/1 Completed 0 18m +openmcp-operator-5f784f47d7-nfg65 1/1 Running 0 18m +ps-managedcontrolplane-668c99c97c-9jltx 1/1 Running 0 18m +ps-managedcontrolplane-init-49rx2 0/1 Completed 0 18m +sp-crossplane-6b8cccc775-9hx98 1/1 Running 0 105s +sp-crossplane-init-6hvf4 0/1 Completed 0 2m11s +``` + +You should see that the crossplane service provider is running. This means that from now on, the openMCP is able to provide Crossplane service instances, using the new service provider Crossplane. + +## Create a Crossplane service instance on the onboarding cluster + +Create a file named `crossplane-instance.yaml` with the following content in the configuration folder: + +```yaml title="config/crossplane-instance.yaml" +apiVersion: crossplane.services.openmcp.cloud/v1alpha1 +kind: Crossplane +metadata: + name: my-mcp + namespace: default +spec: + version: v1.20.0 + providers: + - name: provider-kubernetes + version: v0.16.0 +``` + +Apply the file to onboarding cluster: + +```shell +kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig apply -f ./config/crossplane-instance.yaml +``` + +The Crossplane service provider should now start to create the necessary resources for the new Crossplane instance. As a result, a new Crossplane service instance should soon be available. +You can check the status of the Crossplane instance using the following command: + +```shell +kubectl --kubeconfig ./kubeconfigs/onboarding.kubeconfig get crossplane -n default my-mcp -o yaml +``` + +After a while, you should see output similar to the following: + +```yaml +apiVersion: crossplane.services.openmcp.cloud/v1alpha1 +kind: Crossplane +metadata: + finalizers: + - openmcp.cloud/finalizers + generation: 1 + name: sample + namespace: default +spec: + providers: + - name: provider-kubernetes + version: v0.16.0 + version: v1.20.0 +status: + conditions: + - lastTransitionTime: "2025-09-16T14:09:56Z" + message: Crossplane is healthy. + reason: Healthy + status: "True" + type: CrossplaneReady + - lastTransitionTime: "2025-09-16T14:10:01Z" + message: ProviderKubernetes is healthy. + reason: Healthy + status: "True" + type: ProviderKubernetesReady + observedGeneration: 0 + phase: "" +``` + +Crossplane and the provider Kubernetes should now be available on the mcp cluster. + +```shell +kubectl --kubeconfig ./kubeconfigs/my-mcp.kubeconfig api-resources | grep 'crossplane\|kubernetes' +``` + + + + +## Inspect the Kustomizations in the platform cluster + +After running the bootstrapper for Gardener, verify the deployment status. + +Force an update of the GitRepository and Kustomization in the platform cluster to pick up the changes made in the Git repository. + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system annotate gitrepository environments reconcile.fluxcd.io/requestedAt="$(date +%s)" +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig -n flux-system patch kustomization flux-system --type merge -p '{"spec":{"force":true}}' +``` + +Get the status of the GitRepository: + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get gitrepositories.source.toolkit.fluxcd.io -A +``` + +Get the status of the Kustomizations: + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get kustomizations.kustomize.toolkit.fluxcd.io -A +``` + +You should see the `flux-system` and `bootstrap` Kustomizations in Ready state. + +## Inspect the deployed openMCP components + +Check the deployed openMCP components: + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get pods -n openmcp-system +``` + +You should see the openmcp-operator, managedcontrolplane platform service, and the gardener cluster provider running. + +## Get Access to the Onboarding Cluster + +Check that the onboarding cluster has been created: + +```shell +kubectl --kubeconfig ./kubeconfigs/platform.kubeconfig get clusters.clusters.openmcp.cloud -A +``` + +For Gardener, retrieve the onboarding cluster kubeconfig using the Gardener API or dashboard, then save it to `./kubeconfigs/onboarding.kubeconfig`. + +## Create a Managed Control Plane + +Follow the same steps as the Kind provider to create a managed control plane on the onboarding cluster. + + + diff --git a/docs/reference/00-overview.md b/docs/reference/00-overview.md new file mode 100644 index 0000000..892b8e1 --- /dev/null +++ b/docs/reference/00-overview.md @@ -0,0 +1,175 @@ +--- +sidebar_position: 1 +--- + +# CRD Reference + +Browse the Custom Resource Definitions (CRDs) that power OpenControlPlane. Each CRD has its own dedicated page with an interactive schema viewer, real examples, and complete definitions. + +## End User Resources + +Resources that end users interact with to consume platform capabilities. + +
+ +
+
+ ManagedControlPlane +
+

ManagedControlPlaneV2

+

The primary resource for creating and managing control planes in OpenControlPlane.

+ View CRD → +
+ +
+
+
+ + +
+
dev
+
+

Workspace

+

Isolated environment within a project for deploying and managing applications.

+ View CRD → +
+ +
+
+
+
+ + +
dev
+
+
+ + +
prod
+
+
+
team-project
+
+

Project

+

Organizational unit for grouping workspaces and managing resources across teams.

+ View CRD → +
+ +
+
+ ServiceProvider +
+

ServiceProvider

+

Delivers consumable services to customers via ManagedControlPlanes.

+ View CRD → +
+ +
+ +## Service Providers + +Available service providers that can be deployed within control planes. + +
+ +
+
+ Crossplane +
+

Crossplane

+

Infrastructure provisioning and composition service using Crossplane.

+ View CRD → +
+ +
+
+ Landscaper +
+

Landscaper

+

Declarative deployment orchestration service using Landscaper.

+ View CRD → +
+ +
+
+ Velero +
+

Velero

+

Backup and disaster recovery service using Velero.

+ View CRD → +
+ +
+ +## Operator Resources + +Resources that platform operators use to configure and manage the platform infrastructure. + +### General resources + +
+ +
+
+ ClusterProvider +
+

ClusterProvider

+

Manages Kubernetes clusters and provides access within the ecosystem.

+ View CRD → +
+ +
+
+ PlatformService +
+

PlatformService

+

Delivers complete platform capabilities and services.

+ View CRD → +
+ +
+ +### Cluster Resources + +Resources for managing cluster access and configuration. + +
+ +
+

Cluster

+

Represents a Kubernetes cluster within OpenControlPlane.

+ View CRD → +
+ +
+

ClusterRequest

+

Request for cluster creation or modification.

+ View CRD → +
+ +
+

AccessRequest

+

Request access to a cluster or control plane.

+ View CRD → +
+ +
+

ClusterProfile

+

Defines reusable cluster configuration templates.

+ View CRD → +
+ +
+ +--- + +## Source Code + +CRD definitions are maintained across multiple repositories: + +- **Core & Operator CRDs**: [openmcp-operator](https://github.com/openmcp-project/openmcp-operator/tree/main/api/crds/manifests) +- **Project & Workspace CRDs**: [project-workspace-operator](https://github.com/openmcp-project/project-workspace-operator/tree/main/api/crds/manifests) +- **Service Providers**: + - Crossplane: [service-provider-crossplane](https://github.com/openmcp-project/service-provider-crossplane/tree/main/api/crds/manifests) + - Landscaper: [service-provider-landscaper](https://github.com/openmcp-project/service-provider-landscaper/tree/main/api/crds/manifests) + - Velero: [service-provider-velero](https://github.com/openmcp-project/service-provider-velero/tree/main/api/crds/manifests) diff --git a/docs/reference/core/_category_.json b/docs/reference/core/_category_.json new file mode 100644 index 0000000..11dd4f2 --- /dev/null +++ b/docs/reference/core/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "End User", + "position": 1 +} diff --git a/docs/reference/core/managedcontrolplane.md b/docs/reference/core/managedcontrolplane.md new file mode 100644 index 0000000..90d3c0b --- /dev/null +++ b/docs/reference/core/managedcontrolplane.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 1 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# ManagedControlPlaneV2 + +
+ ManagedControlPlane +
+

The primary resource for creating and managing control planes in OpenControlPlane. Supports IAM configuration with OIDC and token-based authentication.

+
+
+ +**API Group:** `core.openmcp.cloud` +**API Version:** `v2alpha1` +**Kind:** `ManagedControlPlaneV2` + + + +## Usage + +Create a managed control plane with IAM configuration: + +```yaml +apiVersion: core.openmcp.cloud/v2alpha1 +kind: ManagedControlPlaneV2 +metadata: + name: my-controlplane + namespace: default +spec: + iam: + oidc: + defaultProvider: + roleBindings: + - role: cluster-admin +``` + +Access the control plane after creation using the kubeconfig from the status field. diff --git a/docs/reference/core/project.md b/docs/reference/core/project.md new file mode 100644 index 0000000..61be3c4 --- /dev/null +++ b/docs/reference/core/project.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 2 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# Project + +
+
+
+
+ + +
dev
+
+
+ + +
prod
+
+
+
team-project
+
+
+

Organizational unit for grouping workspaces and managing resources across teams. Projects provide multi-tenancy and resource isolation.

+
+
+ +**API Group:** `core.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `Project` + + + +## Usage + +Create a project to organize workspaces and resources: + +```yaml +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Project +metadata: + name: my-project +spec: + displayName: "My Project" + description: "Project for team resources" +``` + +Projects can contain multiple workspaces and provide namespace isolation for different teams or applications. diff --git a/docs/reference/core/workspace.md b/docs/reference/core/workspace.md new file mode 100644 index 0000000..6bb34e0 --- /dev/null +++ b/docs/reference/core/workspace.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 3 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# Workspace + +
+
+
+ + +
+
dev
+
+
+

Isolated environment within a project for deploying and managing applications. Workspaces provide dedicated namespaces and resource quotas.

+
+
+ +**API Group:** `core.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `Workspace` + + + +## Usage + +Create a workspace within a project: + +```yaml +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Workspace +metadata: + name: development + namespace: my-project +spec: + displayName: "Development Environment" + description: "Workspace for development workloads" +``` + +Workspaces are namespace-scoped and belong to a project. They provide isolated environments for different stages of your application lifecycle (dev, staging, production). diff --git a/docs/reference/operator/_category_.json b/docs/reference/operator/_category_.json new file mode 100644 index 0000000..388dfd7 --- /dev/null +++ b/docs/reference/operator/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Operator", + "position": 2 +} diff --git a/docs/reference/operator/clusters/_category_.json b/docs/reference/operator/clusters/_category_.json new file mode 100644 index 0000000..7769be9 --- /dev/null +++ b/docs/reference/operator/clusters/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Clusters", + "position": 1 +} diff --git a/docs/reference/operator/clusters/accessrequest.md b/docs/reference/operator/clusters/accessrequest.md new file mode 100644 index 0000000..0ba0f1e --- /dev/null +++ b/docs/reference/operator/clusters/accessrequest.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 3 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# AccessRequest + +Request access to a cluster or control plane. Manages access permissions and credential generation for users and services. + +**API Group:** `clusters.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `AccessRequest` + + diff --git a/docs/reference/operator/clusters/cluster.md b/docs/reference/operator/clusters/cluster.md new file mode 100644 index 0000000..67f946a --- /dev/null +++ b/docs/reference/operator/clusters/cluster.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 1 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# Cluster + +Represents a Kubernetes cluster within OpenControlPlane. Defines cluster configuration, credentials, and connection details. + +**API Group:** `clusters.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `Cluster` + + diff --git a/docs/reference/operator/clusters/clusterprofile.md b/docs/reference/operator/clusters/clusterprofile.md new file mode 100644 index 0000000..6ab97ef --- /dev/null +++ b/docs/reference/operator/clusters/clusterprofile.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 4 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# ClusterProfile + +Defines reusable cluster configuration templates. Enables consistent cluster provisioning across different environments. + +**API Group:** `clusters.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `ClusterProfile` + + diff --git a/docs/reference/operator/clusters/clusterrequest.md b/docs/reference/operator/clusters/clusterrequest.md new file mode 100644 index 0000000..70312fb --- /dev/null +++ b/docs/reference/operator/clusters/clusterrequest.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 2 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# ClusterRequest + +Request for cluster creation or modification. Initiates cluster lifecycle operations through configured cluster providers. + +**API Group:** `clusters.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `ClusterRequest` + + diff --git a/docs/reference/operator/providers/_category_.json b/docs/reference/operator/providers/_category_.json new file mode 100644 index 0000000..ea99747 --- /dev/null +++ b/docs/reference/operator/providers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Providers", + "position": 2 +} diff --git a/docs/reference/operator/providers/clusterprovider.md b/docs/reference/operator/providers/clusterprovider.md new file mode 100644 index 0000000..24714db --- /dev/null +++ b/docs/reference/operator/providers/clusterprovider.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 2 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# ClusterProvider + +
+ ClusterProvider +
+

Manages Kubernetes clusters and provides access within the OpenControlPlane ecosystem. Handles cluster lifecycle operations and access management.

+
+
+ +**API Group:** `openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `ClusterProvider` + + + +## Related Resources + +- [Build a Cluster Provider](/developers/clusterprovider/develop) +- [Cluster Provider Examples](/developers/clusterprovider/examples) diff --git a/docs/reference/operator/providers/platformservice.md b/docs/reference/operator/providers/platformservice.md new file mode 100644 index 0000000..3816c69 --- /dev/null +++ b/docs/reference/operator/providers/platformservice.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 3 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# PlatformService + +
+ PlatformService +
+

Delivers complete platform capabilities and services. Provides comprehensive platform functionality for OpenControlPlane deployments.

+
+
+ +**API Group:** `openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `PlatformService` + + diff --git a/docs/reference/operator/providers/serviceprovider.md b/docs/reference/operator/providers/serviceprovider.md new file mode 100644 index 0000000..5756fc5 --- /dev/null +++ b/docs/reference/operator/providers/serviceprovider.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 1 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# ServiceProvider + +
+ ServiceProvider +
+

Delivers consumable services to customers via ManagedControlPlanes. Service providers enable platform operators to offer managed services to end users.

+
+
+ +**API Group:** `openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `ServiceProvider` + + + +## Related Resources + +- [Build a Service Provider](/developers/serviceprovider/service-providers) +- [Service Provider Examples](/developers/serviceprovider/examples) diff --git a/docs/reference/services/_category_.json b/docs/reference/services/_category_.json new file mode 100644 index 0000000..6ec29fd --- /dev/null +++ b/docs/reference/services/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Service Providers", + "position": 4 +} diff --git a/docs/reference/services/crossplane.md b/docs/reference/services/crossplane.md new file mode 100644 index 0000000..975f63f --- /dev/null +++ b/docs/reference/services/crossplane.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 1 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# Crossplane + +
+ Crossplane +
+

Delivers Crossplane as a service within ManagedControlPlanes, enabling infrastructure provisioning through composition.

+
+
+ +**API Group:** `crossplane.services.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `Crossplane` + + + +## Usage + +Deploy Crossplane within a control plane: + +```yaml +apiVersion: crossplane.services.openmcp.cloud/v1alpha1 +kind: Crossplane +metadata: + name: my-crossplane + namespace: my-workspace +spec: + crossplaneVersion: "1.14.0" + providerConfigs: + - name: default +``` + +The Crossplane service provider manages the installation and lifecycle of Crossplane and its providers within your control plane. diff --git a/docs/reference/services/landscaper.md b/docs/reference/services/landscaper.md new file mode 100644 index 0000000..860d725 --- /dev/null +++ b/docs/reference/services/landscaper.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 2 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# Landscaper + +
+ Landscaper +
+

Delivers Landscaper as a service within ManagedControlPlanes, enabling declarative deployment orchestration.

+
+
+ +**API Group:** `landscaper.services.openmcp.cloud` +**API Version:** `v1alpha2` +**Kind:** `Landscaper` + + + +## Usage + +Deploy Landscaper within a control plane: + +```yaml +apiVersion: landscaper.services.openmcp.cloud/v1alpha2 +kind: Landscaper +metadata: + name: my-landscaper + namespace: my-workspace +spec: + version: "v0.50.0" + components: + - name: core + enabled: true +``` + +The Landscaper service provider manages the installation and configuration of Landscaper for deployment orchestration. diff --git a/docs/reference/services/velero.md b/docs/reference/services/velero.md new file mode 100644 index 0000000..8088cc0 --- /dev/null +++ b/docs/reference/services/velero.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 3 +--- + +import CRDViewerCompact from '@site/src/components/CRDViewerCompact'; + +# Velero + +
+ Velero +
+

Delivers Velero as a service for backup and disaster recovery within ManagedControlPlanes.

+
+
+ +**API Group:** `velero.services.openmcp.cloud` +**API Version:** `v1alpha1` +**Kind:** `Velero` + + + +## Usage + +Deploy Velero for backup and recovery: + +```yaml +apiVersion: velero.services.openmcp.cloud/v1alpha1 +kind: Velero +metadata: + name: backup-service + namespace: my-workspace +spec: + backupStorageLocation: + provider: aws + bucket: my-backups +``` + +The Velero service provider manages backup and disaster recovery capabilities for your control planes. diff --git a/docs/users/00-getting-started.md b/docs/users/00-getting-started.md index a7f41c2..67cb7cb 100644 --- a/docs/users/00-getting-started.md +++ b/docs/users/00-getting-started.md @@ -1,350 +1,67 @@ -# Getting Started +--- +sidebar_position: 0 +--- -In this guide we will go through how to request a service instance via the onboarding cluster and consume it on the ManagedControlPlane cluster. +# Welcome -## Prerequisites +OpenControlPlane is a platform that lets you create and manage Kubernetes-based control planes for your teams. Think of it as a way to deliver cloud services to your organization—everything from databases and message queues to CI/CD pipelines and monitoring tools—all through a unified Kubernetes API. -In order to get started, the first you will require is a kubeconfig pointing to your onboarding cluster. This is the cluster in which -users can create Projects, Workspaces and request MCP Cluster instances. Once this kubeconfig is acquired you can proceed with the rest of this guide. +:::info Prerequisites +Requires a deployed OpenControlPlane platform. Operators: see [setup guide](/operators/setup) → [verify setup](/operators/verify-setup). +::: -Note, that normally, you will only have limited access to resources in the on-boarding cluster. -This means, you won't able to list existing resources of most kinds, but you will be able to create the resource that you will actually require. +## What You'll Learn -## Setup +This guide will help you understand and use OpenControlPlane effectively: -### 1. Create a `Project` +### Getting Started -A `Project` is the starting point of your Manged Control Plane (MCP) journey. It is a logical grouping of `Workspaces` and `ManagedControlPlanes`. A `Project` can be used to represent an organization, department, team or any other logical grouping of resources. +Learn the basics of working with OpenControlPlane: +- **[Onboarding](./getting-started/onboard)** - Create your first project and workspace +- **[Connect](./getting-started/connect)** - Access your control plane +- **[Configure](./getting-started/configure)** - Set up services and resources -_**Note**_, that in your organization certain `annotations` or `labels` might be required to be set in order to have a correct Workspace. Please contact your cluster administrator to find out more. -For example, certain SAP specific labels are as follows: -```yaml -labels: - openmcp.cloud.sap/charging-target: "" - openmcp.cloud.sap/charging-target-type: "" -``` +### Core Concepts -The values for these are provided by your cluster administrator. +Understand the building blocks: +- **[Managed Control Plane](./concepts/managed-control-plane)** - Your dedicated Kubernetes API server +- **[Projects & Workspaces](./concepts/managed-control-plane)** - Organize teams and environments +- **[Service Providers](./concepts/service-provider)** - Deploy services like Crossplane or Landscaper -A normal project can look something like this: +### Ecosystem -```yaml -apiVersion: core.openmcp.cloud/v1alpha1 -kind: Project -metadata: - name: platform-team - annotations: - openmcp.cloud/display-name: Platform Team -spec: - members: - - kind: User - name: first.user@example.com - roles: - - admin - - kind: User - name: second.user@example.com - roles: - - view -``` +Explore the [open-source projects](./ecosystem) that power OpenControlPlane, including Kubernetes, Crossplane, Gardener, and Landscaper. -To create it, run: +## Quick Navigation -``` -kubectl create -f project.yaml -``` +
-_**Note**_: We are using `create` here for a reason. This goes for the rest of this guide. +
+

New User?

+

Start with our step-by-step onboarding guide.

+ Get Started → +
-Once the project reconciles, check the project status. It should contain a `namespace` section that the project generated. +
+

Learn Concepts

+

Understand how OpenControlPlane works.

+ Core Concepts → +
-To check the status, you should have access to list your specific project with: +
+

Browse CRDs

+

Explore the complete API reference with examples.

+ CRD Browser → +
-``` -$> kubectl describe project ocm-team +
-Name: platform-team -Namespace: -... -API Version: core.openmcp.cloud/v1alpha1 -Kind: Project -Metadata: - Creation Timestamp: 2026-03-10T12:02:37Z - Finalizers: - core.openmcp.cloud - Generation: 1 - Resource Version: 140594720 - UID: 0566ecc4-72f0-4905-904c-cc609fcfc014 -Spec: - Members: - Kind: User - Name: - Roles: - admin - Kind: User - Name: - Roles: - admin -Status: - Namespace: project-platform-team -Events: -``` +## Need Help? -### 2. Create a `Workspace` in the `Project` +- **Community**: Join our [community hub](/community/overview) to connect with other users +- **GitHub**: Report issues or browse repositories at [openmcp-project](https://github.com/openmcp-project) +- **Support**: Check the [contributing guide](https://github.com/openmcp-project/community/blob/main/CONTRIBUTING.md) for ways to get help -A `Workspace` is a logical grouping of `ManagedControlPlanes`. A `Workspace` can be used to represent an environment (e.g. dev, staging, prod) or again an organization, department, team or any other logical grouping of resources. +--- -The create a workspace you can use the following configuration: - -```yaml -apiVersion: core.openmcp.cloud/v1alpha1 -kind: Workspace -metadata: - name: dev - namespace: project-platform-team # This is retrieved from the Project status from above. - annotations: - openmcp.cloud/display-name: Platform Team - Dev -spec: - members: - - kind: User - name: first.user@example.com - roles: - - admin - - kind: User - name: second.user@example.com - roles: - - view -``` - -Note, that the namespace in which the Workspace lives in is from the Project created above. Create this workspace by running: - -``` -kubectl create -f workspace.yaml -``` - -The output of this object will also contain a namespace. That namespace is the specific namespace to use for your MCP cluster creation! - -You inspect the resource by running the following command: - -``` -$> kubectl describe workspace dev -n project-platform-team - -Name: dev -Namespace: project-platform-team -Labels: -Annotations: core.openmcp.cloud/created-by: - openmcp.cloud/display-name: Platform Team - Dev -API Version: core.openmcp.cloud/v1alpha1 -Kind: Workspace -Metadata: - Creation Timestamp: 2026-03-10T12:02:51Z - Finalizers: - core.openmcp.cloud - Generation: 1 - Resource Version: 140594791 - UID: 9d52be65-0c71-4ab4-85b4-dcf20e12fa7f -Spec: - Members: - Kind: User - Name: - Roles: - admin - Kind: User - Name: - Roles: - admin -Status: - Namespace: project-platform-team--ws-dev -Events: -``` - -Grab that namespace and continue with creating the ManagedControlPlane resource. - -### 3. Create a `ManagedControlPlane` in the `Workspace` - -The `ManagedControlPlane` resource is the heart of the openMCP platform. Each Managed Control Plane (MCP) has its own Kubernetes API endpoint and data store. You can use the `iam` property to define who should have access to the MCP and the resources it contains. - -The create a ManagedControlPlane object create the following yaml: - -_**Note**_: The name of this object is significant and will be used later. Choose carefully. - -```yaml -apiVersion: core.openmcp.cloud/v2alpha1 -kind: ManagedControlPlaneV2 -metadata: - name: mcp-01 - namespace: project-platform-team--ws-dev -spec: - iam: - oidc: # for human authentication - defaultProvider: - roleBindings: # authorization for human users - - roleRefs: - - kind: ClusterRole - name: cluster-admin - subjects: - - kind: User - name: first.user@example.com - - kind: User - name: second.user@example.com - tokens: # for machine authentication - - name: xyz-service-token - roleRefs: # authorization for machine users - - kind: ClusterRole - name: cluster-admin -``` - -Under `spec.iam` you can define the authentication for your ManagedControlPlane. You can use OIDC-based authentication for human users and token-based authentication for machine users. -For authorization, ClusterRoleBindings will map the specified roles to the defined subjects. For token-based authentication, the specified roles will get bound to a generated ServiceAccount on the ManagedControlPlane. - -Normally, you would only require one of these, so don't worry if one of them says failed to reconcile, while the other is `Ready`. - -Once the cluster is successfully reconciled, in the status at `status.access` you will find the references to the secrets at the Onboarding API that contains the kubeconfig to access your MCP for the OIDC and/or token-based authentication methods. - -On this cluster, your user is an Admin user. You should be able to have access to all the installed resources. - -Next, is how you request an actual service to be present on your MCP cluster. - -_**Note**_: If any of the needed resources to install a specific service provider do not exist on your onboarding cluster, ask your cluster administrator to install the required CRDs via a `ServiceProvider` resource. - -Once the `ManageControlPlane` object reconciles, you should see something like this: - -``` -$> kubectl describe mcpv2 mcp-01 -n project-ocm-team--ws-canary - -Name: mcp-01 -Namespace: project-platform-team--ws-dev -Labels: -Annotations: -API Version: core.openmcp.cloud/v2alpha1 -Kind: ManagedControlPlaneV2 -Metadata: - Creation Timestamp: 2026-03-13T09:36:31Z - ... -Spec: - Iam: - Oidc: - Default Provider: - Role Bindings: - Role Refs: - Kind: ClusterRole - Name: cluster-admin - Subjects: - Kind: User - Name: first.user@example.com - Kind: User - Name: second.user@example.com - Tokens: - Name: xyz-service-token - Role Refs: - Kind: ClusterRole - Name: cluster-admin -Status: - Access: - oidc_openmcp: - Name: oidc-openmcp.mcp-01.kubeconfig - token_xyz-service-token: - Name: token-xyz-service-token.mcp-01.kubeconfig - Conditions: - Last Transition Time: 2026-03-13T13:25:00Z - Message: - ... -``` - -Note the `status.access` resource under `oidc_openmcp`. This is the Secret you need to fetch in order to get your kubeconfig for the provisioned MCP cluster. - -To fetch that value and put it into a file called `mcp-kubeconfig.yaml`, run the following command: - -``` -kubectl get secrets "$(kubectl get mcpv2 mcp-01 -n project-platform-team--ws-dev -o jsonpath='{.status.access.oidc_openmcp.name}')" -n project-platform-team--ws-dev -o jsonpath='{.data.kubeconfig}' | base64 -d > mcp-kubeconfig.yaml -``` - -### 4. Install managed services in your Managed Control Plane (MCP) - -You can install managed services in your Managed Control Plane (MCP) to extend its functionality. Currently, the following managed services are available: -- Crossplane via the [service-provider-crossplane](https://github.com/openmcp-project/service-provider-crossplane) -- Landscaper via the [service-provider-landscaper](https://github.com/openmcp-project/service-provider-landscaper) -- OCM via the [service-provider-ocm](https://github.com/open-component-model/service-provider-ocm) - -#### Prerequisites - -In order to install any of the above offerings, their `ProviderConfig` object must exist in your onboarding cluster. Each service will have a specific `ProviderConfig` object that you -can get from the service provider's repository. Please contact your onboarding cluster administrator to install the necessary configurations and the `ServiceProvider` objects. - -_**Note**_: For each of these providers the `name` of the managed service object _MUST_ match your MCP object's name. This is so that a single MCP cluster cannot have multiple installations of the same provider. - -#### Managed Service: Crossplane - -Crossplane is an open source project that enables you to manage cloud infrastructure and services using Kubernetes-style declarative configuration. It allows you to define and manage cloud resources such as databases, storage, and networking using Kubernetes manifests. - -To install Crossplane in your MCP, you need to create a `Crossplane` resource in the same namespace as your `ManagedControlPlane`. The following example installs Crossplane version `v1.20.0` with the `provider-kubernetes` provider version `v0.16.0`. - -```yaml -apiVersion: crossplane.services.openmcp.cloud/v1alpha1 -kind: Crossplane -metadata: - name: mcp-01 # Same name as your ManagedControlPlane - namespace: project-platform-team--ws-dev # Same namespace as your ManagedControlPlane -spec: - version: v1.20.0 - providers: - - name: provider-kubernetes - version: v0.16.0 -``` - -#### Managed Service: Landscaper - -Landscaper manages the installation, updates, and uninstallation of cloud-native workloads, with focus on larger complexities, while being capable of handling complex dependency chains between the individual components. - -To install a Landscaper for your MCP, you need to create a `Landscaper` resource with the same namespace and name as your `ManagedControlPlane`. The following example installs the Landscaper with default configuration. - -```yaml -apiVersion: landscaper.services.openmcp.cloud/v1alpha2 -kind: Landscaper -metadata: - name: mcp-01 # Same name as your ManagedControlPlane - namespace: project-platform-team--ws-dev # Same namespace as your ManagedControlPlane -spec: - version: v0.142.0 -``` - -#### Managed Service: OCM - -The Open Component Model (OCM) toolset helps you deliver and deploy your software securely anywhere, at any scale. It's an open standard that defines deliverable in components that then can be further -processed transferred and verified to any location regardless of the technology of storage. - -To install its operator for your MCP cluster, you need to create an `OCM` resource in the same namespace and name as your `ManagedControlPlane` object. - -```yaml -apiVersion: ocm.services.openmcp.cloud/v1alpha1 -kind: OCM -metadata: - name: mcp-01 # must match your MCP cluster so it will track the right cluster - namespace: project-platform-team--ws-dev -spec: - version: 0.2.0 -``` - -Once this object reconciles, you should see something like this in its status: - -``` -# Make sure you are using the kubeconfig for that MCP cluster -$> kubectl describe pod -n ocm-k8s-toolkit-system ocm-k8s-toolkit-controller-manager- - -Name: ocm-k8s-toolkit-controller-manager-68b94b65bc-8ggv8 -Namespace: ocm-k8s-toolkit-system -Priority: 0 -Service Account: ocm-k8s-toolkit-controller-manager -... -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Normal Scheduled 56s default-scheduler Successfully assigned ocm-k8s-toolkit-system/ocm-k8s-toolkit-controller-manager-68b94b65bc-8ggv8 to *** - Normal Pulling 56s kubelet Pulling image "ghcr.io/open-component-model/kubernetes/controller:0.2.0@sha256:78ffe14f5175e3510f6dfb20df0a07eeb2de99ee24e56a0015dd941727b1c9e7" - Normal Pulled 53s kubelet Successfully pulled image "ghcr.io/open-component-model/kubernetes/controller:0.2.0@sha256:78ffe14f5175e3510f6dfb20df0a07eeb2de99ee24e56a0015dd941727b1c9e7" in 2.718s (2.718s including waiting). Image size: 34158077 bytes. - Normal Created 53s kubelet Created container: manager - Normal Started 53s kubelet Started container manager -``` - -The base ocm installation comes with a bare minimum set of RBAC settings. To extend this, simply follow our guide here: [custom RBAC for OCM](https://github.com/open-component-model/open-component-model/blob/main/kubernetes/controller/docs/getting-started/custom-rbac.md). - -Since you are an admin on the MCP cluster, extending the service account's RBAC should work. \ No newline at end of file +Ready to get started? Head to the [onboarding guide](./getting-started/onboard) to create your first control plane. diff --git a/docs/about/concepts/_category_.yml b/docs/users/concepts/_category_.yml similarity index 100% rename from docs/about/concepts/_category_.yml rename to docs/users/concepts/_category_.yml diff --git a/docs/users/concepts/cluster-provider.md b/docs/users/concepts/cluster-provider.md new file mode 100644 index 0000000..a4c33cf --- /dev/null +++ b/docs/users/concepts/cluster-provider.md @@ -0,0 +1,3 @@ +# Cluster Providers + +Cluster providers are responsible for the dynamic creation, modification, and deletion of Kubernetes clusters in an OpenControlPlane environment. They conceal certain cluster technologies (e.g., [Gardener](https://gardener.cloud/) and [Kubernetes-in-Docker](https://kind.sigs.k8s.io/)) behind a homogeneous interface. This allows operators to install an OpenControlPlane system in different environments and on various infrastructure providers without having to adjust the other components of the system accordingly. diff --git a/docs/users/concepts/managed-control-plane.md b/docs/users/concepts/managed-control-plane.md new file mode 100644 index 0000000..3a98e1e --- /dev/null +++ b/docs/users/concepts/managed-control-plane.md @@ -0,0 +1,3 @@ +# Managed Control Planes (MCPs) + +Managed Control Planes (MCPs) are at the heart of OpenControlPlane. Simply put, they are lightweight Kubernetes clusters that store the desired state and current status of various resources. All resources follow the Kubernetes Resource Model (KRM), allowing infrastructure resources, deployments, etc., to be managed with common Kubernetes tools like kubectl, kustomize, Helm, Flux, ArgoCD, and so on. diff --git a/docs/users/concepts/platform-service.md b/docs/users/concepts/platform-service.md new file mode 100644 index 0000000..bcff4e6 --- /dev/null +++ b/docs/users/concepts/platform-service.md @@ -0,0 +1,3 @@ +# Platform Services + +Platform services add functionality to an OpenControlPlane environment (not MCPs). Examples include network services (Gateway API, Ingress), audit logs, billing, grouping of MCPs, and system-wide policies. They are installed and configured by the platform operator and apply to the entire system. diff --git a/docs/users/concepts/service-provider.md b/docs/users/concepts/service-provider.md new file mode 100644 index 0000000..3c0966f --- /dev/null +++ b/docs/users/concepts/service-provider.md @@ -0,0 +1,3 @@ +# Service Providers + +Without service providers, MCPs are of little use. They add functionality such as cloud provider APIs, GitOps, policies, or backup and restore to MCPs. The operators of an OpenControlPlane environment decide which service providers are available to end users. The end users can then activate them for their MCPs. diff --git a/docs/about/design/_category_.yml b/docs/users/design/_category_.yml similarity index 100% rename from docs/about/design/_category_.yml rename to docs/users/design/_category_.yml diff --git a/docs/users/ecosystem.md b/docs/users/ecosystem.md new file mode 100644 index 0000000..a8395e4 --- /dev/null +++ b/docs/users/ecosystem.md @@ -0,0 +1,143 @@ +--- +sidebar_position: 2 +--- + +# Ecosystem + +OpenControlPlane is built on top of amazing open-source projects from the cloud native ecosystem. Here are the key projects that power our platform. + +
+ +
+
+ Kubernetes +

Kubernetes

+
+
+ The foundation of OpenControlPlane. We extend the Kubernetes API through Custom Resource Definitions (CRDs), enabling you to configure infrastructure, services, and applications using the same familiar API. +
+ +
+ +
+
+ Crossplane +

Crossplane

+
+
+ A CNCF project that orchestrates anything through Kubernetes. Enable Crossplane as a service provider to give your users access to the rich ecosystem of Crossplane providers. +
+ +
+
We endorse using these Crossplane providers:
+
+ AWS + Azure + GCP + SAP BTP +
+
+
+ +
+
+ Gardener +

Gardener

+
+
+ Delivers fully-managed Kubernetes clusters at scale across AWS, Azure, GCP, OpenStack, and more. Use Gardener as a cluster provider in OpenControlPlane for automated cluster management. +
+ +
+ +
+
+ Flux +

Flux

+
+
+ Continuous and progressive delivery for Kubernetes. Enable Flux to provide GitOps capabilities in your Managed Control Planes, allowing declarative infrastructure management from Git. +
+ +
+ +
+
+ Kyverno +

Kyverno

+
+
+ Policy-as-Code for Kubernetes and cloud native environments. Define team-internal and organization-wide policies to establish security standards and corporate compliance requirements. +
+ +
+ +
+
+ External Secrets +

External Secrets

+
+
+ Integrates external secret management systems like AWS Secrets Manager, HashiCorp Vault, and more. Automatically sync secrets into Kubernetes or push generated secrets to external systems. +
+ +
+ +
+
+ Open Component Model +

Open Component Model

+
+
+ An open standard for describing software artifacts and lifecycle metadata in a technology-agnostic way. Used by OpenControlPlane to package and deliver components reliably to any environment. +
+ +
+ +
+
+ Landscaper +

Landscaper

+
+
+ Describes, installs, and maintains cloud-native landscapes. Activate as a service provider to simplify the rollout of complex software products for your users with declarative installations. +
+ +
+ +
+ +## Why These Projects? + +All of these projects share OpenControlPlane's commitment to: + +- **Open Standards**: Built on Kubernetes and cloud native principles +- **Extensibility**: Designed to be extended and customized +- **Declarative Management**: Infrastructure and services as code +- **Community-Driven**: Active CNCF and open-source communities + +By building on these proven foundations, OpenControlPlane provides a robust, scalable platform for managing your cloud infrastructure and services. diff --git a/docs/users/getting-started/01-onboard.md b/docs/users/getting-started/01-onboard.md new file mode 100644 index 0000000..2b44706 --- /dev/null +++ b/docs/users/getting-started/01-onboard.md @@ -0,0 +1,216 @@ +--- +sidebar_position: 1 +--- + +# 1. Onboard + +This guide walks you through creating the foundational resources for your OpenControlPlane setup: Project, Workspace, and ControlPlane. + +:::info Prerequisites +Requires a deployed OpenControlPlane platform. Operators: see [setup guide](/operators/setup) → [verify setup](/operators/verify-setup). +::: + +## Understanding the Hierarchy + +OpenControlPlane organizes resources in a three-level hierarchy: + +```mermaid +flowchart TD + subgraph OnboardingAPI["Onboarding API"] + P["Project
platform-team"] + + subgraph NS1["project-platform-team"] + W1["Workspace
dev"] + W2["Workspace
prod"] + end + + subgraph NS2["project-platform-team--ws-dev"] + M1["ControlPlane
my-controlplane"] + M2["ControlPlane
another-cp"] + end + + subgraph NS3["project-platform-team--ws-prod"] + M3["ControlPlane
prod-cp"] + end + end + + P --> W1 + P --> W2 + W1 --> M1 + W1 --> M2 + W2 --> M3 + + style P fill:#2CE0BF,stroke:#07838F,color:#012931 + style W1 fill:#C2FCEE,stroke:#049F9A,color:#02414C + style W2 fill:#C2FCEE,stroke:#049F9A,color:#02414C + style M1 fill:#fff,stroke:#07838F,color:#02414C + style M2 fill:#fff,stroke:#07838F,color:#02414C + style M3 fill:#fff,stroke:#07838F,color:#02414C +``` + +- **Project** — Top-level organization unit (team, department, or org) +- **Workspace** — Environment separation within a project (dev, staging, prod) +- **ControlPlane** — Your actual Kubernetes API endpoint with its own data store + +## Prerequisites + +Before you begin, ensure you have: + +| Requirement | Description | +|-------------|-------------| +| **Onboarding API access** | Your platform operator provides the API endpoint and credentials | +| **kubectl** | Version 1.25 or later ([install guide](https://kubernetes.io/docs/tasks/tools/)) | +| **kubeconfig** | Configured to connect to the Onboarding API | + +:::tip Platform Access +If you don't have access to an OpenControlPlane installation, contact your platform operator. Operators can follow the [Bootstrapping Guide](../../operators/00-overview.md) to set up a new environment. +::: + +--- + +## Step 1: Create a Project + +A `Project` is the starting point of your ControlPlane journey. It's a logical grouping of `Workspaces` and `ControlPlanes`. Use a Project to represent an organization, department, team, or any other logical grouping. + +```yaml +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Project +metadata: + name: platform-team + annotations: + openmcp.cloud/display-name: Platform Team +spec: + members: + - kind: User + name: first.user@example.com + roles: + - admin + - kind: User + name: second.user@example.com + roles: + - view +``` + +Apply it to the Onboarding API: + +```bash +kubectl apply -f project.yaml +``` + +--- + +## Step 2: Create a Workspace + +A `Workspace` is a logical grouping of `ControlPlanes`. Use Workspaces to represent environments (dev, staging, prod) or other organizational boundaries. + +```yaml +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Workspace +metadata: + name: dev + namespace: project-platform-team + annotations: + openmcp.cloud/display-name: Platform Team - Dev +spec: + members: + - kind: User + name: first.user@example.com + roles: + - admin + - kind: User + name: second.user@example.com + roles: + - view +``` + +:::info Namespace Convention +Workspaces live in a namespace named `project-`. For example, a Workspace in the `platform-team` Project goes in the `project-platform-team` namespace. +::: + +```bash +kubectl apply -f workspace.yaml +``` + +--- + +## Step 3: Create a ControlPlane + +The `ControlPlane` resource is the heart of OpenControlPlane. Each ControlPlane has its own Kubernetes API endpoint and data store. You can use the `iam` property to define who can access the ControlPlane. + +```yaml +apiVersion: core.openmcp.cloud/v2alpha1 +kind: ManagedControlPlaneV2 +metadata: + name: my-controlplane + namespace: project-platform-team--ws-dev +spec: + iam: + oidc: + defaultProvider: + roleBindings: + - roleRefs: + - kind: ClusterRole + name: cluster-admin + subjects: + - kind: User + name: first.user@example.com + - kind: User + name: second.user@example.com + tokens: + - name: ci-service-token + roleRefs: + - kind: ClusterRole + name: cluster-admin +``` + +:::info Namespace Convention +ControlPlanes live in a namespace named `project---ws-`. For example, a ControlPlane in the `dev` Workspace of the `platform-team` Project goes in `project-platform-team--ws-dev`. +::: + +### Authentication & Authorization + +The `spec.iam` section controls who can access your ControlPlane and what they can do. + +#### Human Authentication (OIDC) + +For users authenticating through your identity provider: + +```yaml +iam: + oidc: + defaultProvider: + roleBindings: + - roleRefs: + - kind: ClusterRole + name: cluster-admin + subjects: + - kind: User + name: alice@example.com +``` + +OpenControlPlane creates ClusterRoleBindings in your ControlPlane based on these specifications. + +#### Machine Authentication (Tokens) + +For CI/CD pipelines and service accounts: + +```yaml +iam: + tokens: + - name: ci-service-token + roleRefs: + - kind: ClusterRole + name: cluster-admin +``` + +For token-based auth, a ServiceAccount is automatically generated and bound to the specified roles. + +```bash +kubectl apply -f controlplane.yaml +``` + +--- + +## Next Steps + +Continue to [2. Connect](./02-connect.md) to retrieve credentials and access your ControlPlane. diff --git a/docs/users/getting-started/02-connect.md b/docs/users/getting-started/02-connect.md new file mode 100644 index 0000000..371d7ae --- /dev/null +++ b/docs/users/getting-started/02-connect.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 2 +--- + +# 2. Connect + +This guide shows you how to retrieve credentials and connect to your ControlPlane using kubectl. + +## Check ControlPlane Status + +First, verify your ControlPlane is ready: + +```bash +kubectl get managedcontrolplanev2 my-controlplane -n project-platform-team--ws-dev +``` + +Wait until the ControlPlane shows a ready status. The `status.access` field contains references to your credentials. + +## Retrieve Your Kubeconfig + +The ControlPlane creates secrets containing kubeconfig files for each authentication method you configured. + +### For OIDC Authentication (Human Users) + +```bash +# Get the secret name from status +SECRET_NAME=$(kubectl get managedcontrolplanev2 my-controlplane \ + -n project-platform-team--ws-dev \ + -o jsonpath='{.status.access.oidc.secretRef.name}') + +# Retrieve and decode the kubeconfig +kubectl get secret $SECRET_NAME -n project-platform-team--ws-dev \ + -o jsonpath='{.data.kubeconfig}' | base64 -d > my-controlplane-oidc.kubeconfig +``` + +### For Token Authentication (Machine Users) + +```bash +# Get the secret name from status +SECRET_NAME=$(kubectl get managedcontrolplanev2 my-controlplane \ + -n project-platform-team--ws-dev \ + -o jsonpath='{.status.access.tokens[0].secretRef.name}') + +# Retrieve and decode the kubeconfig +kubectl get secret $SECRET_NAME -n project-platform-team--ws-dev \ + -o jsonpath='{.data.kubeconfig}' | base64 -d > my-controlplane-token.kubeconfig +``` + +## Verify Access + +Test your connection to the ControlPlane: + +```bash +# Using the retrieved kubeconfig +kubectl --kubeconfig=my-controlplane-oidc.kubeconfig get namespaces +``` + +You should see the default Kubernetes namespaces, confirming your ControlPlane is accessible. + +:::tip Set as Default Context +To use your ControlPlane as the default context: +```bash +export KUBECONFIG=my-controlplane-oidc.kubeconfig +kubectl get namespaces +``` +::: + +--- + +## Next Steps + +Continue to [3. Configure](./03-configure.md) to install managed services and extend your ControlPlane functionality. diff --git a/docs/users/getting-started/03-configure.md b/docs/users/getting-started/03-configure.md new file mode 100644 index 0000000..e94f085 --- /dev/null +++ b/docs/users/getting-started/03-configure.md @@ -0,0 +1,75 @@ +--- +sidebar_position: 3 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 3. Configure + +This guide shows you how to install managed services in your ControlPlane to extend its functionality. + +## Install Managed Services + +You can install managed services in your ControlPlane to add capabilities like infrastructure management and workload orchestration. + + + + +[Crossplane](https://www.crossplane.io/) enables you to manage cloud infrastructure using Kubernetes-style declarative configuration. + +To install Crossplane, create a `Crossplane` resource in the same namespace as your ControlPlane: + +```yaml +apiVersion: crossplane.services.openmcp.cloud/v1alpha1 +kind: Crossplane +metadata: + name: my-controlplane + namespace: project-platform-team--ws-dev +spec: + version: v1.20.0 + providers: + - name: provider-kubernetes + version: v0.16.0 +``` + +The `name` must match your ControlPlane name. + +```bash +kubectl apply -f crossplane.yaml +``` + + + + +[Landscaper](https://github.com/gardener/landscaper) manages the installation, updates, and uninstallation of cloud-native workloads with complex dependency chains. + +To install Landscaper, create a `Landscaper` resource: + +```yaml +apiVersion: landscaper.services.openmcp.cloud/v1alpha2 +kind: Landscaper +metadata: + name: my-controlplane + namespace: project-platform-team--ws-dev +spec: + version: v0.142.0 +``` + +```bash +kubectl apply -f landscaper.yaml +``` + + + + +--- + +## Next Steps + +Congratulations! You have a working ControlPlane with managed services. Here's what you can explore next: + +- **[What is a Managed Control Plane?](../concepts/managed-control-plane.md)** — Deeper understanding of ControlPlanes +- **[Service Providers](../concepts/service-provider.md)** — How managed services work +- **[Crossplane Service Provider](https://github.com/openmcp-project/service-provider-crossplane)** — Manage cloud infrastructure +- **[Landscaper Service Provider](https://github.com/openmcp-project/service-provider-landscaper)** — Orchestrate complex workloads diff --git a/docs/users/getting-started/_category_.json b/docs/users/getting-started/_category_.json new file mode 100644 index 0000000..b12ab68 --- /dev/null +++ b/docs/users/getting-started/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Order Control Plane", + "position": 1 +} diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 36b4cef..300b9d2 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -5,7 +5,7 @@ import type * as Preset from '@docusaurus/preset-classic'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) const config: Config = { - title: 'Open Managed Control Plane (openMCP)', + title: 'Open Control Plane', tagline: 'Part of ApeiroRA and NeoNephos.', favicon: 'img/favicon.ico', @@ -45,6 +45,8 @@ const config: Config = { sidebarPath: './sidebars.ts', editUrl: 'https://github.com/openmcp-project/docs/tree/main/', + sidebarCollapsible: true, + sidebarCollapsed: true, }, blog: { routeBasePath: "adrs", @@ -72,39 +74,44 @@ const config: Config = { themeConfig: { // Replace with your project's social card - image: 'img/docusaurus-social-card.jpg', + image: 'img/co_axolotl.png', navbar: { - title: 'Open Managed Control Plane (openMCP)', + title: 'Open Control Plane', logo: { - alt: 'My Site Logo', - src: 'img/logo.svg', + alt: 'Open Control Plane Logo', + src: 'img/co_axolotl_mirrored.png', }, items: [ - { - type: 'docSidebar', - sidebarId: 'about', - position: 'left', - label: 'About OpenMCP', - }, { type: 'docSidebar', sidebarId: 'userDocs', position: 'left', - label: 'End-users', + label: 'Get Started', }, { type: 'docSidebar', sidebarId: 'operatorDocs', position: 'left', - label: 'Operators', + label: 'Run Your Platform', }, { type: 'docSidebar', sidebarId: 'developerDocs', position: 'left', - label: 'Developers', + label: 'Build Together', + }, + { + type: 'docSidebar', + sidebarId: 'communitySidebar', + position: 'right', + label: 'Community', + }, + { + type: 'docSidebar', + sidebarId: 'referenceSidebar', + position: 'right', + label: 'CRD Browser', }, - {to: '/adrs', label: 'ADRs', position: 'left'}, { href: 'https://github.com/openmcp-project/docs', label: 'GitHub', @@ -134,6 +141,10 @@ const config: Config = { { label: 'NeoNephos', href: 'https://neonephos.org/', + }, + { + label: 'Crossplane Provider Community @ SAP', + href: 'https://github.com/SAP/crossplane-provider-docs', }, ], }, @@ -156,7 +167,7 @@ const config: Config = { }, ], copyright: ` - Copyright © ${new Date().getFullYear()} SAP SE or an SAP affiliate company and openMCP contributors. + Copyright © ${new Date().getFullYear()} SAP SE or an SAP affiliate company and openControlPlane contributors.
This site is hosted by GitHub Pages. Please see the GitHub Privacy Statement for any information how GitHub processes your personal data. diff --git a/package-lock.json b/package-lock.json index cc2c13e..ae2fcbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@docusaurus/theme-mermaid": "^3.8.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "js-yaml": "^4.1.1", + "lucide-react": "^0.577.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -10466,9 +10468,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10805,6 +10807,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/markdown-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", diff --git a/package.json b/package.json index 5ea4320..14294c4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@docusaurus/theme-mermaid": "^3.8.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "js-yaml": "^4.1.1", + "lucide-react": "^0.577.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/sidebars.ts b/sidebars.ts index 2b8608f..acbca7c 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -13,10 +13,11 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; Create as many sidebars as you want. */ const sidebars: SidebarsConfig = { - about: [{type: 'autogenerated', dirName: 'about'}], userDocs: [{type: 'autogenerated', dirName: 'users'}], operatorDocs: [{type: 'autogenerated', dirName: 'operators'}], developerDocs: [{type: 'autogenerated', dirName: 'developers'}], + communitySidebar: [{type: 'autogenerated', dirName: 'community'}], + referenceSidebar: [{type: 'autogenerated', dirName: 'reference'}], }; export default sidebars; diff --git a/src/components/CRDViewer/index.js b/src/components/CRDViewer/index.js new file mode 100644 index 0000000..8c9b696 --- /dev/null +++ b/src/components/CRDViewer/index.js @@ -0,0 +1,147 @@ +import React, { useState, useEffect } from 'react'; +import CodeBlock from '@theme/CodeBlock'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import yaml from 'js-yaml'; + +export default function CRDViewer({ crdUrl, name, description, exampleUrl }) { + const [crdData, setCrdData] = useState(null); + const [exampleData, setExampleData] = useState(null); + const [schema, setSchema] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + try { + // Fetch CRD + const crdResponse = await fetch(crdUrl); + const crdText = await crdResponse.text(); + setCrdData(crdText); + + // Parse CRD YAML to extract OpenAPI schema + const crdObject = yaml.load(crdText); + const specSchema = crdObject?.spec?.versions?.[0]?.schema?.openAPIV3Schema?.properties?.spec; + + if (specSchema) { + setSchema(parseSchema(specSchema)); + } + + // Fetch example if provided + if (exampleUrl) { + const exampleResponse = await fetch(exampleUrl); + const exampleText = await exampleResponse.text(); + setExampleData(exampleText); + } + } catch (err) { + setError(err.message); + } + setLoading(false); + }; + + loadData(); + }, [crdUrl, exampleUrl]); + + const parseSchema = (schemaObj, prefix = '') => { + const fields = []; + + if (!schemaObj || !schemaObj.properties) { + return fields; + } + + Object.entries(schemaObj.properties).forEach(([key, value]) => { + const fieldName = prefix ? `${prefix}.${key}` : key; + const type = getType(value); + const description = value.description || 'No description available'; + const required = schemaObj.required?.includes(key) || false; + + fields.push({ + name: fieldName, + type: type, + description: description, + required: required + }); + + // Recursively parse nested objects + if (value.type === 'object' && value.properties) { + fields.push(...parseSchema(value, fieldName)); + } + + // Handle arrays of objects + if (value.type === 'array' && value.items?.properties) { + fields.push(...parseSchema(value.items, `${fieldName}[]`)); + } + }); + + return fields; + }; + + const getType = (value) => { + if (value.type === 'array' && value.items) { + if (value.items.type) { + return `${value.items.type}[]`; + } + return 'array'; + } + return value.type || 'unknown'; + }; + + if (loading) return
Loading CRD...
; + if (error) return
Error loading CRD: {error}
; + if (!crdData) return null; + + return ( +
+
+

{name}

+

{description}

+
+ + + +
+ {schema && schema.length > 0 ? ( + + + + + + + + + + + {schema.map((prop, idx) => ( + + + + + + + ))} + +
FieldTypeRequiredDescription
{prop.name}{prop.type}{prop.required ? Yes : 'No'}{prop.description}
+ ) : ( +

Schema information not available. Please refer to the Full Definition tab.

+ )} +
+
+ + {exampleData && ( + + + {exampleData} + + + )} + + + + {crdData} + + +
+
+ ); +} diff --git a/src/components/CRDViewerCompact/index.js b/src/components/CRDViewerCompact/index.js new file mode 100644 index 0000000..99048d8 --- /dev/null +++ b/src/components/CRDViewerCompact/index.js @@ -0,0 +1,231 @@ +import React, { useState, useEffect } from 'react'; +import CodeBlock from '@theme/CodeBlock'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import { Search } from 'lucide-react'; +import yaml from 'js-yaml'; + +const SchemaField = ({ field, depth = 0, searchTerm }) => { + const hasChildren = field.children && field.children.length > 0; + + // Check if any children are required + const hasRequiredChildren = field.children?.some(child => + child.required || hasRequiredDescendants(child) + ); + + // Auto-expand if: field is required, has required children, or matches search + const shouldAutoExpand = field.required || hasRequiredChildren; + + const [isExpanded, setIsExpanded] = useState(shouldAutoExpand); + + // Check if this field or any children match the search + const matchesSearch = searchTerm === '' || + field.name.toLowerCase().includes(searchTerm.toLowerCase()) || + field.description.toLowerCase().includes(searchTerm.toLowerCase()); + + const childrenMatch = field.children?.some(child => + child.name.toLowerCase().includes(searchTerm.toLowerCase()) || + child.description.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // Auto-expand if search matches children + useEffect(() => { + if (searchTerm && childrenMatch) { + setIsExpanded(true); + } + }, [searchTerm, childrenMatch]); + + // Helper function to check if any descendants are required + function hasRequiredDescendants(node) { + if (!node.children || node.children.length === 0) return false; + return node.children.some(child => + child.required || hasRequiredDescendants(child) + ); + } + + if (!matchesSearch && !childrenMatch) { + return null; + } + + return ( +
+
hasChildren && setIsExpanded(!isExpanded)}> + {hasChildren && ( + + ▶ + + )} + {field.name} + {field.type} + {field.required && required} +
+ + {field.description && ( +
{field.description}
+ )} + + {hasChildren && isExpanded && ( +
+ {field.children.map((child, idx) => ( + + ))} +
+ )} +
+ ); +}; + +export default function CRDViewerCompact({ crdUrl, name, description, exampleUrl }) { + const [crdData, setCrdData] = useState(null); + const [exampleData, setExampleData] = useState(null); + const [schema, setSchema] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + try { + const crdResponse = await fetch(crdUrl); + const crdText = await crdResponse.text(); + setCrdData(crdText); + + const crdObject = yaml.load(crdText); + const specSchema = crdObject?.spec?.versions?.[0]?.schema?.openAPIV3Schema?.properties?.spec; + const statusSchema = crdObject?.spec?.versions?.[0]?.schema?.openAPIV3Schema?.properties?.status; + + const parsedFields = []; + if (specSchema) { + parsedFields.push({ + name: 'spec', + type: 'object', + description: 'Specification of the desired behavior', + required: true, + children: parseSchemaToTree(specSchema) + }); + } + if (statusSchema) { + parsedFields.push({ + name: 'status', + type: 'object', + description: 'Observed status of the resource', + required: false, + children: parseSchemaToTree(statusSchema) + }); + } + + setSchema(parsedFields); + + if (exampleUrl) { + const exampleResponse = await fetch(exampleUrl); + const exampleText = await exampleResponse.text(); + setExampleData(exampleText); + } + } catch (err) { + setError(err.message); + } + setLoading(false); + }; + + loadData(); + }, [crdUrl, exampleUrl]); + + const parseSchemaToTree = (schemaObj) => { + if (!schemaObj || !schemaObj.properties) { + return []; + } + + return Object.entries(schemaObj.properties).map(([key, value]) => { + const field = { + name: key, + type: getType(value), + description: value.description || '', + required: schemaObj.required?.includes(key) || false, + children: [] + }; + + // Recursively parse nested objects + if (value.type === 'object' && value.properties) { + field.children = parseSchemaToTree(value); + } + + // Handle arrays of objects + if (value.type === 'array' && value.items?.properties) { + field.children = parseSchemaToTree(value.items); + } + + return field; + }); + }; + + const getType = (value) => { + if (value.type === 'array' && value.items) { + if (value.items.type) { + return `[]${value.items.type}`; + } + if (value.items.properties) { + return '[]object'; + } + return '[]'; + } + return value.type || 'unknown'; + }; + + if (loading) return
Loading CRD...
; + if (error) return
Error loading CRD: {error}
; + if (!crdData) return null; + + return ( +
+ + +
+
+ + setSearchTerm(e.target.value)} + className="schema-search-input" + /> +
+
+
+ {schema.map((field, idx) => ( + + ))} +
+
+ + {exampleData && ( + + + Real example from the repository ↗ + + } + > + {exampleData} + + + )} + + + + {crdData} + + +
+
+ ); +} diff --git a/src/components/IconContainer/index.js b/src/components/IconContainer/index.js new file mode 100644 index 0000000..65183e4 --- /dev/null +++ b/src/components/IconContainer/index.js @@ -0,0 +1,18 @@ +import React from 'react'; + +export default function IconContainer({ children, size = 60, compact = false }) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/src/css/custom.css b/src/css/custom.css index 2bc6a4c..5d74139 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -4,27 +4,7323 @@ * work well for content-centric websites. */ -/* You can override the default Infima variables here. */ +/* ===== Design tokens & IFM vars ===== */ :root { - --ifm-color-primary: #2e8555; - --ifm-color-primary-dark: #29784c; - --ifm-color-primary-darker: #277148; - --ifm-color-primary-darkest: #205d3b; - --ifm-color-primary-light: #33925d; - --ifm-color-primary-lighter: #359962; - --ifm-color-primary-lightest: #3cad6e; + /* Landing Page Core Colors */ + --lp-c-white: #ffffff; + --lp-c-black: #000000; + --lp-c-gray-1: #515c67; + --lp-c-gray-2: #414853; + --lp-c-gray-3: #32363f; + --lp-c-gray-soft: rgba(101, 117, 133, 0.16); + + /* Landing Page Dark Theme Colors */ + --lp-c-bg: #1b1b1f; + --lp-c-bg-alt: #161618; + --lp-c-bg-elv: #202127; + --lp-c-bg-soft: #202127; + --lp-c-border: #3c3f44; + --lp-c-divider: #2e2e32; + --lp-c-gutter: #000000; + --lp-c-text-1: #dfdfd6; + --lp-c-text-2: #98989f; + --lp-c-text-3: #6a6a71; + + /* Brand Colors */ + --lp-c-green-1: #1e8f95; + --lp-c-green-2: #32bcac; + --lp-c-green-3: #3fdec0; + --lp-c-green-soft: rgba(16, 185, 129, 0.16); + --lp-c-brand-1: var(--lp-c-green-1); + --lp-c-brand-2: var(--lp-c-green-2); + --lp-c-brand-3: var(--lp-c-green-3); + --lp-c-brand-soft: var(--lp-c-green-soft); + + /* Role-based Teal Spectrum */ + --teal-2: #C2FCEE; + --teal-4: #2CE0BF; + --teal-6: #049F9A; + --teal-7: #07838F; + --teal-10: #02414C; + --teal-11: #012931; + + /* Role Colors - Light to Dark gradient for End User -> Operator -> Contributor */ + --role-enduser-primary: var(--teal-7); + --role-enduser-secondary: var(--teal-10); + --role-operator-primary: var(--teal-10); + --role-operator-secondary: var(--teal-11); + --role-contributor-primary: transparent; + --role-contributor-secondary: transparent; + --role-contributor-border: var(--teal-11); + + /* Button Colors */ + --lp-button-brand-bg: #009f76; + --lp-button-brand-border: transparent; + --lp-button-brand-text: var(--lp-c-white); + --lp-button-brand-hover-bg: var(--lp-c-brand-2); + --lp-button-alt-bg: var(--lp-c-gray-3); + --lp-button-alt-text: var(--lp-c-text-1); + --lp-button-alt-hover-bg: var(--lp-c-gray-2); + + /* Hero Specific */ + --lp-home-hero-name-color: transparent; + --lp-home-hero-name-background: -webkit-linear-gradient(120deg, #1e8f95 30%, #3fdec0); + --lp-home-hero-image-background-image: linear-gradient(-45deg, #1e8f95 50%, #3fdec0 50%); + --lp-home-hero-image-filter: blur(68px); + + /* Typography */ + --lp-font-family-base: + "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --lp-font-family-mono: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace; + + /* ===== BASE IFM/Docusaurus variables ===== */ + --ifm-color-primary: #1e8f95; + --ifm-color-primary-dark: #1a7f84; + --ifm-color-primary-darker: #19777c; + --ifm-color-primary-darkest: #146166; + --ifm-color-primary-light: #229fa6; + --ifm-color-primary-lighter: #24a7ae; + --ifm-color-primary-lightest: #2bbdc5; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --ifm-table-cell-padding: 0.5rem; + --ifm-hr-margin-vertical: 0.5rem; + --ifm-list-margin: 0.5rem; + --ifm-navbar-padding-vertical: 0; + --ifm-navbar-height: 3rem; + --ifm-blockquote-border-left-width: 6px; + --doc-sidebar-width: 250px !important; + --ifm-spacing-horizontal: 8px; + --ifm-list-left-padding: 1.5rem; + --prism-background-color: #1a2534; + --ifm-h1-font-size: 2.5rem; + --ifm-h2-font-size: 1.8rem; + --ifm-h4-font-size: 1.2rem; + --ifm-h6-font-size: 1rem; + + /* ===== navbar variables ===== */ + --c-accent: #55e9e9; + --c-accent-contrast: #081012; + --c-text-strong: #0a0f12; + --c-text: #1b242a; + --c-text-dim: #5a6a73; + --c-bg: #ffffff; + --c-bg-elev: #f6f8f9; + --c-card: #ffffff; + --c-sep: #e7ecef; + + --shadow-lg: 0 20px 60px rgba(4, 13, 18, 0.18); + --shadow-md: 0 10px 30px rgba(4, 13, 18, 0.14); + + --container: 1200px; + --pad-x: 24px; + --pad-x-lg: 32px; + + --h1: 56px; + --h1-lh: 1.05; + --h2: 36px; + --h2-lh: 1.15; + + --bg-rgb: 11, 15, 19; + --glass-alpha: 0.99; + --nav-border-scrolled: rgba(255, 255, 255, 0.16); + --navbar-height-fallback: 64px; + --navbar-bg-blur: 12px; + --container-nav: 1320px; + --nav-gap: clamp(22px, 3.6vw, 46px); + --nav-link-size: clamp(14px, 1.05vw, 16px); + --nav-pad-x: 24px; } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; + --ifm-color-primary: #3fdec0; + --ifm-color-primary-dark: #29d3b3; + --ifm-color-primary-darker: #22ceac; + --ifm-color-primary-darkest: #1aaf93; + --ifm-color-primary-light: #55e3c7; + --ifm-color-primary-lighter: #62e6cc; + --ifm-color-primary-lightest: #87edd9; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } + +[data-theme='dark'] .image-container > .image-src { + filter: drop-shadow(-2px 4px 6px rgba(0, 0, 0, 0.3)); +} + +/* ===== Navbar: transparent-on-top effect (landing page) ===== */ +.navbar { + transition: background-color 0.5s, border-bottom-color 0.5s, box-shadow 0.5s; + z-index: 200 !important; +} + +.navbar--transparent { + background-color: transparent !important; + border-bottom-color: transparent !important; + box-shadow: none !important; +} + +/* ===== Landing Page: Hero ===== */ +.lp-home-top { + height: 60vh; + width: 100vw; + object-fit: cover; + position: absolute; + opacity: 0; + z-index: 0; +} + +.lp-home { + background-color: var(--ifm-background-color); + min-height: 10vh; + gap: 24px; + position: relative; + z-index: 1; +} + +.lp-home .container { + display: flex; + margin: 0 auto; + max-width: 1152px; + flex-direction: column; + text-align: center; + gap: 64px; +} + +.lp-home .flex-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 16px; + max-width: 1152px; + width: 100%; + height: 60vh; + margin: 0 auto; + padding: 0 24px; +} + +.lp-home .main { + flex: 1 1 100%; + order: 2; +} + +.lp-home .heading { + line-height: 1.2; + font-weight: 600; + margin-bottom: 24px; +} + +.lp-home .name { + font-size: 48px; + display: block; +} + +.lp-home .name.clip { + background: var(--lp-home-hero-name-background); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: var(--lp-home-hero-name-color); +} + +.lp-home .text { + font-size: 28px !important; + font-weight: 500; + color: var(--ifm-font-color-base); + display: block; + margin-top: 8px; +} + +.lp-home .tagline { + color: var(--lp-c-text-2); + font-size: 20px; + line-height: 30px; + margin: 0 auto 32px; + max-width: 600px; +} + +.lp-home .definition { + color: var(--c-text-dim); + font-size: 14px; + line-height: 22px; + margin: 24px auto 0; + max-width: 850px; + opacity: 0.85; + text-align: center; +} + +.lp-home .actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; + z-index: 1; +} + +/* Hero media */ +.lp-home .image { + order: 1; + flex: 1 1 100%; + justify-content: center; + align-items: center; +} + +.lp-home .image-container { + position: relative; + width: 450px; + height: 450px; + margin: 60px auto 0; +} + +.lp-home .image-bg { + position: absolute; + inset: 0; + background-image: var(--lp-home-hero-image-background-image); + filter: var(--lp-home-hero-image-filter); + border-radius: 50%; + opacity: 0.6; +} + +.image-container > .image-src { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + object-fit: contain; + z-index: 1; + transform-origin: 50% 55%; + animation: lp-axolotl-float 10s ease-in-out infinite; + filter: drop-shadow(-2px 4px 6px rgba(0, 0, 0, 0.25)); +} + +@keyframes lp-axolotl-float { + 0% { + transform: translate(0, 0) rotate(0deg); + } + 25% { + transform: translate(3px, -4px) rotate(-0.8deg); + } + 50% { + transform: translate(-3px, 3px) rotate(0.7deg); + } + 75% { + transform: translate(2px, 4px) rotate(0.5deg); + } + 100% { + transform: translate(0, 0) rotate(0deg); + } +} + +@media (prefers-reduced-motion: reduce) { + .image-container > .image-src { + animation: none; + } +} + +/* Hero media queries */ +@media (max-width: 480px) { + .lp-home .container { + gap: var(--nav-pad-x) !important; + } + .lp-home .flex-container { + flex-direction: column; + height: 80vh; + } + .lp-home .actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + width: 100%; + } + .action a { + padding: 6px 8px; + font-size: 12px; + } +} + +@media (min-width: 481px) and (max-width: 959px) { + .lp-home .image-container { + width: 280px; + height: 280px; + } + .lp-home .flex-container { + flex-direction: column; + } +} + +@media (min-width: 768px) { + .lp-home .name { + font-size: 56px; + } + .lp-home .text { + font-size: 36px; + } +} + +@media (min-width: 960px) { + .lp-home .flex-container { + height: 50vh; + } + .lp-home .main { + flex: 1 1 50%; + order: 1; + text-align: left; + } + .lp-home .actions { + justify-content: flex-start; + } + .lp-home .image { + flex: 1 1 50%; + order: 2; + } +} + +/* ===== Action buttons ===== */ +.action p { + margin: 0; + padding: 0; +} + +.action a { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + line-height: 1.2; + height: auto; + min-width: 0; + white-space: nowrap; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + text-decoration: none !important; + border: 1px solid transparent; + transition: + transform 0.2s ease, + box-shadow 0.2s ease, + background-color 0.25s ease, + color 0.25s ease; + width: 100%; +} + +/* Legacy brand/alt button styles for other buttons */ +.action.brand a { + background: var(--lp-c-green-3); + color: black; + border-color: transparent; +} + +.action.brand a:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25); + color: black; +} + +.action.brand a:active { + transform: translateY(0); + box-shadow: none; +} + +.action.alt a { + background: var(--lp-c-green-1); + color: black; + border-color: transparent; +} + +.action.alt a:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25); + color: black; +} + +.action.alt a:active { + transform: translateY(0); +} + +/* Role-specific hero button colors - MUST come after generic .action.alt a */ +.lp-home .action.alt a[href*="/users/"], +.lp-home .action a[href*="/users/"] { + background: var(--role-enduser-primary) !important; + color: white !important; + border-color: transparent; +} + +.lp-home .action.alt a[href*="/users/"]:hover, +.lp-home .action a[href*="/users/"]:hover { + background: var(--role-enduser-secondary) !important; + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(7, 131, 143, 0.4); + color: white !important; +} + +.lp-home .action.alt a[href*="/operators/"], +.lp-home .action a[href*="/operators/"] { + background: var(--role-operator-primary) !important; + color: white !important; + border-color: transparent; +} + +.lp-home .action.alt a[href*="/operators/"]:hover, +.lp-home .action a[href*="/operators/"]:hover { + background: var(--role-operator-secondary) !important; + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(2, 65, 76, 0.4); + color: white !important; +} + +.lp-home .action.alt a[href*="/developers/"], +.lp-home .action a[href*="/developers/"] { + background: transparent !important; + color: white !important; + border: none !important; +} + +.lp-home .action.alt a[href*="/developers/"]:hover, +.lp-home .action a[href*="/developers/"]:hover { + background: rgba(1, 41, 49, 0.3) !important; + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(1, 41, 49, 0.4); + color: white !important; +} + +.action a:active { + transform: translateY(0); + box-shadow: none; +} + +.lp-home .actions { + gap: 10px; +} + +/* ===== Feature cards ===== */ +.lp-features { + display: grid; + margin: 60px auto 0; + grid-template-columns: 1fr; + gap: 14px; +} + +.lp-features h3 { + font-size: 16px; + color: var(--lp-c-text-1) !important; +} + +.lp-feature-card { + border-radius: 12px; + padding: 16px 28px 24px 28px; + position: relative; + background-color: #f6f6f7; + border: 1px solid #f6f6f7; + transition: border-color 0.25s, background-color 0.25s; + overflow: hidden; + isolation: isolate; +} + +.lp-feature-card .mouse-glow { + position: absolute; + width: 420px; + height: 420px; + border-radius: 12px; + background: radial-gradient(circle, rgba(30, 143, 149, 0.45) 0%, rgba(194, 252, 238, 0.25) 60%, transparent 100%); + filter: blur(48px); + transform: translate(-50%, -50%); + z-index: 1; + opacity: 0; + transition: opacity 0.2s; + pointer-events: none; +} + +.lp-feature-card:hover .mouse-glow { + opacity: 0.95; +} + +.lp-feature-card > *:not(.mouse-glow) { + position: relative; + z-index: 2; +} + +@media (max-width: 640px) { + .lp-feature-card .mouse-glow { + display: none; + } +} + +.lp-feature-card img { + width: 72px; + height: 72px; + margin-bottom: 16px; +} + +.lp-feature-card p { + margin: 0; + color: var(--ifm-color-emphasis-700); + font-weight: 500; + line-height: 1.6; + font-size: 14px; +} + +@media (max-width: 480px) { + .lp-features { + grid-template-columns: 1fr; + gap: 20px; + width: 100%; + } + .lp-feature-card { + text-align: left; + } +} + +@media (min-width: 481px) and (max-width: 959px) { + .lp-features { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + width: 100%; + } +} + +@media (min-width: 960px) { + .lp-features { + grid-template-columns: repeat(3, 1fr); + gap: 14px; + max-width: 1152px; + margin-left: auto; + margin-right: auto; + } +} + +@media (min-width: 1152px) { + .lp-feature-card { + padding: 12px 28px 20px 28px; + } +} + +/* ===== Get started section ===== */ +.get-started-section { + background-color: var(--ifm-background-color); + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + padding: 64px 24px; + height: 24vh; +} + +.gray-white { + background-color: var(--ifm-background-surface-color); +} + +/* ===== Open-source section ===== */ +.open-source-section { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 24px 48px; + background-color: var(--ifm-background-color); + color: var(--ifm-font-color-base); +} + +.open-source-section > .col { + max-width: 1152px; + width: 100%; + flex: none; +} + +.open-source-section p, +.open-source-section p b { + color: inherit; +} + +.open-source-section a { + color: var(--ifm-color-primary); +} + +.open-source-wrapper { + height: 16vh; + display: grid; + place-items: center; +} + +.typing-open-source { + width: 17.5ch; + white-space: nowrap; + overflow: hidden; + border-right: 3px solid; + font-family: monospace; + font-size: 4em; + opacity: 0; +} + +.typing-open-source.animate { + opacity: 1; + animation: typing 1.4s steps(17), blink 0.5s step-end infinite alternate; +} + +@keyframes typing { + from { + width: 0; + } +} + +@keyframes blink { + 50% { + border-color: transparent; + } +} + +.landingpage-section-title { + font-family: "72 Black", sans-serif; + text-align: center; + margin-bottom: 0; + width: 100%; +} + +/* ===== Fade-in utility ===== */ +.fadeIn { + opacity: 1; + animation-name: fadeIn; + animation-duration: 1s; + animation-fill-mode: both; +} + +/* ===== Light theme overrides ===== */ +html[data-theme="light"] { + --lp-c-bg: #ffffff; + --lp-c-bg-alt: #f9fbfc; + --lp-c-bg-elv: #ffffff; + --lp-c-bg-soft: #f6f8f9; + --lp-c-border: #e7ecef; + --lp-c-text-1: #0a0f12; + --lp-c-text-2: #1b242a; + --lp-c-text-3: #5a6a73; + --c-text-dim: #5a6a73; + --lp-c-divider: #e2e2de; + --c-accent: #519c8c; + --lp-button-brand-text: #ffffff; + --lp-button-brand-hover-bg: #32bcac; + --lp-button-alt-bg: #eef3f6; + --lp-button-alt-text: var(--lp-c-text-1); + --lp-button-alt-hover-bg: #e6edf3; + --nav-border-scrolled: rgba(0, 0, 0, 0.16); + --lp-shadow-soft: 0 2px 6px rgba(0, 0, 0, 0.08); + --lp-shadow-strong: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +html[data-theme="light"] .lp-home { + background-color: var(--ifm-background-color); +} + +html[data-theme="light"] .lp-home .heading .name { + color: var(--lp-c-text-1); +} + +html[data-theme="light"] .lp-home .tagline { + color: var(--lp-c-text-2) !important; +} + +html[data-theme="light"] .lp-home a { + text-decoration-color: color-mix(in oklab, var(--c-accent) 50%, transparent); +} + +html[data-theme="light"] .lp-home a:hover { + text-decoration-color: var(--c-accent); +} + +html[data-theme="light"] .lp-home .lp-features { + color: var(--lp-c-text-2); +} + +html[data-theme="light"] .lp-home .lp-feature-card h3 { + color: var(--lp-c-text-1) !important; +} + +html[data-theme="light"] .lp-features .lp-feature-card:hover { + background: #eef3f6; +} + +html[data-theme="light"] .lp-home .image-bg { + opacity: 0.3; +} + +/* Light theme: override for button colors */ +html[data-theme="light"] .lp-home .action.alt a[href*="/users/"], +html[data-theme="light"] .lp-home .action a[href*="/users/"] { + background: var(--role-enduser-primary) !important; + color: white !important; +} + +html[data-theme="light"] .lp-home .action.alt a[href*="/users/"]:hover, +html[data-theme="light"] .lp-home .action a[href*="/users/"]:hover { + background: var(--role-enduser-secondary) !important; + color: white !important; +} + +html[data-theme="light"] .lp-home .action.alt a[href*="/operators/"], +html[data-theme="light"] .lp-home .action a[href*="/operators/"] { + background: var(--role-operator-primary) !important; + color: white !important; +} + +html[data-theme="light"] .lp-home .action.alt a[href*="/operators/"]:hover, +html[data-theme="light"] .lp-home .action a[href*="/operators/"]:hover { + background: var(--role-operator-secondary) !important; + color: white !important; +} + +html[data-theme="light"] .lp-home .action.alt a[href*="/developers/"], +html[data-theme="light"] .lp-home .action a[href*="/developers/"] { + background: transparent !important; + color: var(--teal-11) !important; + border: none !important; +} + +html[data-theme="light"] .lp-home .action.alt a[href*="/developers/"]:hover, +html[data-theme="light"] .lp-home .action a[href*="/developers/"]:hover { + background: rgba(1, 41, 49, 0.1) !important; + color: var(--teal-10) !important; +} + +html[data-theme="light"] .lp-home .actions .action a { + color: #ffffff !important; +} + +html[data-theme="light"] .lp-home .actions .action:hover { + transform: translateY(-2px); +} + +html[data-theme="light"] .lp-home .actions .action:active { + transform: translateY(0); + box-shadow: none; +} + +html[data-theme="light"] .open-source-section { + color: var(--lp-c-text-2); +} + +html[data-theme="light"] .get-started-section { + background-color: var(--ifm-background-color); +} + +/* ===== Dark mode: open-source section ===== */ +html[data-theme="dark"] .open-source-section { + color: var(--lp-c-text-1); +} + +html[data-theme="dark"] .open-source-section p, +html[data-theme="dark"] .open-source-section p b { + color: var(--lp-c-text-1) !important; +} + +html[data-theme="dark"] .open-source-section a { + color: var(--ifm-color-primary) !important; +} + +html[data-theme="dark"] .open-source-section a:hover { + color: var(--ifm-color-primary-light) !important; +} + +html[data-theme="dark"] .typing-open-source { + color: var(--lp-c-green-3); + border-right-color: var(--lp-c-green-3); +} + +/* ===== Dark mode: feature cards ===== */ +html[data-theme="dark"] .lp-features .lp-feature-card { + background-color: var(--ifm-background-surface-color); + border-color: var(--ifm-background-surface-color); +} + +/* ===== Dark mode: get-started section ===== */ +html[data-theme="dark"] .get-started-section { + color: var(--lp-c-text-1); +} + +html[data-theme="dark"] .get-started-section span { + color: var(--lp-c-text-2) !important; +} + +html[data-theme="dark"] .get-started-section a:not(.button) { + color: var(--ifm-color-primary) !important; +} + +/* Dark mode: ensure role button colors are visible */ +html[data-theme="dark"] .lp-home .action.alt a[href*="/users/"], +html[data-theme="dark"] .lp-home .action a[href*="/users/"] { + background: var(--role-enduser-primary) !important; + color: white !important; +} + +html[data-theme="dark"] .lp-home .action.alt a[href*="/users/"]:hover, +html[data-theme="dark"] .lp-home .action a[href*="/users/"]:hover { + background: var(--role-enduser-secondary) !important; + color: white !important; +} + +html[data-theme="dark"] .lp-home .action.alt a[href*="/operators/"], +html[data-theme="dark"] .lp-home .action a[href*="/operators/"] { + background: var(--role-operator-primary) !important; + color: white !important; +} + +html[data-theme="dark"] .lp-home .action.alt a[href*="/operators/"]:hover, +html[data-theme="dark"] .lp-home .action a[href*="/operators/"]:hover { + background: var(--role-operator-secondary) !important; + color: white !important; +} + +html[data-theme="dark"] .lp-home .action.alt a[href*="/developers/"], +html[data-theme="dark"] .lp-home .action a[href*="/developers/"] { + background: transparent !important; + color: white !important; + border: none !important; +} + +html[data-theme="dark"] .lp-home .action.alt a[href*="/developers/"]:hover, +html[data-theme="dark"] .lp-home .action a[href*="/developers/"]:hover { + background: rgba(1, 41, 49, 0.3) !important; + color: white !important; +} + +/* ===== Dark mode: footer ===== */ +html[data-theme="dark"] .footer--dark { + background-color: #161618; + border-top-color: #2e2e32; +} + +html[data-theme="dark"] .footer--dark .footer__title { + color: #dfdfd6; +} + +html[data-theme="dark"] .footer--dark .footer__col { + color: #98989f; +} + +html[data-theme="dark"] .footer--dark .footer__item a, +html[data-theme="dark"] .footer--dark .footer__link-item { + color: #98989f !important; +} + +html[data-theme="dark"] .footer--dark .footer__item a:hover, +html[data-theme="dark"] .footer--dark .footer__link-item:hover { + color: var(--ifm-color-primary) !important; + text-decoration: none; +} + +html[data-theme="dark"] .footer--dark .footer__copyright { + color: #6a6a71; +} + +/* ===== Light mode: footer ===== */ +html[data-theme="light"] .footer--dark { + background-color: #f6f8f9; + border-top-color: #e7ecef; +} + +html[data-theme="light"] .footer--dark .footer__title { + color: #0a0f12; +} + +html[data-theme="light"] .footer--dark .footer__item a, +html[data-theme="light"] .footer--dark .footer__link-item { + color: #1b242a !important; +} + +html[data-theme="light"] .footer--dark .footer__item a:hover, +html[data-theme="light"] .footer--dark .footer__link-item:hover { + color: var(--ifm-color-primary) !important; + text-decoration: none; +} + +html[data-theme="light"] .footer--dark .footer__copyright { + color: #5a6a73; +} + +/* ===== Footer base ===== */ +.footer--dark { + --ifm-footer-background-color: var(--lp-c-bg); + background-color: var(--lp-c-bg); + border-top: 1px solid var(--lp-c-divider); + padding: 32px 0 16px 0; + margin-top: 0; +} + +.footer--dark .footer__copyright { + font-size: 12px; + line-height: 24px; + text-align: center; + padding-top: 16px; + margin-top: 16px; + border-top: 1px solid var(--lp-c-divider); + width: 100%; +} + +.footer--dark svg { + display: none; +} + +/* ===== "See our projects" button in open-source-section ===== */ +.open-source-section .button--primary { + margin-top: 16px; + background-color: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: #ffffff !important; + font-weight: 600; +} + +.open-source-section .button--primary:hover { + background-color: var(--ifm-color-primary-dark); + border-color: var(--ifm-color-primary-dark); + color: #ffffff !important; +} + +html[data-theme="dark"] .open-source-section .button--primary { + background-color: var(--lp-c-green-3); + border-color: var(--lp-c-green-3); + color: #000000 !important; +} + +html[data-theme="dark"] .open-source-section .button--primary:hover { + background-color: var(--lp-c-green-2); + border-color: var(--lp-c-green-2); + color: #000000 !important; +} + +/* ===== Global link colors in light mode ===== */ +html[data-theme="light"] { + --ifm-link-color: var(--c-accent); + --ifm-link-hover-color: var(--c-accent); +} + +html[data-theme="light"] #__docusaurus :where(.markdown, .theme-doc-markdown, .container, article) a:hover { + text-decoration-color: var(--c-accent); +} + +/* ===== Responsive: inner-source on small screens ===== */ +@media only screen and (max-width: 1024px) { + .typing-open-source { + width: 17ch; + font-size: 2em; + } +} + +/* ===== Custom Footer (ocp-footer) ===== */ +.ocp-footer { + background-color: var(--ifm-background-color); + border-top: 1px solid var(--ifm-color-emphasis-200); + font-size: 13px; + line-height: 1.6; + color: var(--ifm-font-color-base); +} + +[data-theme='dark'] .ocp-footer { + border-top-color: #2e2e32; + color: #98989f; +} + +/* EU Banner row */ +.ocp-footer__eu-banner { + background-color: var(--ifm-background-surface-color); + border-bottom: 1px solid var(--ifm-color-emphasis-200); + padding: 20px 0; +} + +[data-theme='dark'] .ocp-footer__eu-banner { + border-bottom-color: #2e2e32; +} + +.ocp-footer__eu-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + display: flex; + align-items: center; + gap: 24px; +} + +.ocp-footer__eu-logos { + flex-shrink: 0; +} + +.ocp-footer__eu-logos img { + max-height: none; + max-width: 300px; + width: 100%; + display: block; +} + +.ocp-footer__eu-text { + flex: 1; + font-size: 11px; + color: #5a6a73; +} + +[data-theme='dark'] .ocp-footer__eu-text { + color: #6a6a71; +} + +.ocp-footer__eu-text p { + margin: 0; +} + +.ocp-footer__eu-text p + p { + margin-top: 4px; +} + +.ocp-footer__neonephos { + flex-shrink: 0; + font-size: 15px; +} + +.ocp-footer__neonephos a { + color: #049F9A; + text-decoration: none; + font-size: 15px; +} + +.ocp-footer__neonephos a:hover { + color: #07838F; + text-decoration: underline; +} + +[data-theme='dark'] .ocp-footer__neonephos a { + color: #2CE0BF; +} + +[data-theme='dark'] .ocp-footer__neonephos a:hover { + color: #3fdec0; +} + +[data-theme='dark'] .ocp-footer__neonephos img { + filter: brightness(0) invert(1); +} + +/* Copyright row */ +.ocp-footer__copyright-row { + border-bottom: 1px solid var(--ifm-color-emphasis-200); + padding: 16px 0; +} + +[data-theme='dark'] .ocp-footer__copyright-row { + border-bottom-color: #2e2e32; +} + +.ocp-footer__inner { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; +} + +.ocp-footer__copyright-row p { + margin: 0; + font-size: 12px; + color: #5a6a73; +} + +[data-theme='dark'] .ocp-footer__copyright-row p { + color: #98989f; +} + +.ocp-footer__copyright-row a { + color: #049F9A; + text-decoration: none; +} + +.ocp-footer__copyright-row a:hover { + text-decoration: underline; + color: #07838F; +} + +[data-theme='dark'] .ocp-footer__copyright-row a { + color: #2CE0BF; +} + +/* Legal links row */ +.ocp-footer__legal-row { + padding: 12px 0; +} + +.ocp-footer__legal-links { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.ocp-footer__legal-links a { + color: #5a6a73; + text-decoration: none; + font-size: 12px; +} + +.ocp-footer__legal-links a:hover { + color: #049F9A; + text-decoration: underline; +} + +[data-theme='dark'] .ocp-footer__legal-links a { + color: #98989f; +} + +[data-theme='dark'] .ocp-footer__legal-links a:hover { + color: #3fdec0; +} + +.ocp-footer__legal-sep { + color: #c0cdd4; + font-size: 12px; + user-select: none; +} + +[data-theme='dark'] .ocp-footer__legal-sep { + color: #6a6a71; +} + +/* Responsive footer */ +@media (max-width: 768px) { + .ocp-footer__eu-container { + flex-direction: column; + align-items: flex-start; + gap: 16px; + } + + .ocp-footer__eu-logos { + align-self: center; + } +} + +/* ===== Ecosystem Page Styling ===== */ + +.ecosystem-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + margin: 32px 0; + max-width: 900px; +} + +.project-card { + background: var(--ifm-card-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 16px; + padding: 28px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + flex-direction: column; + height: 100%; + position: relative; + overflow: hidden; +} + +/* Subtle gradient overlay on card */ +.project-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--teal-6) 0%, var(--teal-4) 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.project-card:hover::before { + opacity: 1; +} + +.project-card:hover { + transform: translateY(-6px); + box-shadow: 0 12px 28px rgba(4, 159, 154, 0.15), 0 4px 12px rgba(4, 159, 154, 0.08); + border-color: var(--teal-4); +} + +[data-theme='dark'] .project-card { + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .project-card:hover { + background: rgba(255, 255, 255, 0.04); + border-color: var(--teal-6); + box-shadow: 0 12px 28px rgba(44, 224, 191, 0.15), 0 4px 12px rgba(44, 224, 191, 0.08); +} + +.project-card-header { + display: flex; + align-items: center; + gap: 18px; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--ifm-color-emphasis-100); +} + +[data-theme='dark'] .project-card-header { + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.project-logo, +img.project-logo, +.project-card-header .project-logo, +.project-card-header img.project-logo, +.project-card-header > img.project-logo, +.ecosystem-grid .project-logo, +.ecosystem-grid img.project-logo, +img[alt="Kubernetes"], +img[alt="Crossplane"], +img[alt="Flux"], +img[alt="Kyverno"], +img[alt="External Secrets"], +img[alt="Open Component Model"], +img[alt="Landscaper"] { + width: 48px !important; + height: 48px !important; + min-width: 48px !important; + min-height: 48px !important; + max-width: 48px !important; + max-height: 48px !important; + object-fit: contain !important; + flex-shrink: 0 !important; + display: block !important; +} + +.project-card-header h3 { + margin: 0; + font-size: 1.35rem; + font-weight: 700; + background: linear-gradient(135deg, var(--teal-7) 0%, var(--teal-10) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +[data-theme='dark'] .project-card-header h3 { + background: linear-gradient(135deg, var(--teal-4) 0%, var(--teal-6) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.project-description { + font-size: 0.975rem; + line-height: 1.65; + margin-bottom: 24px; + flex-grow: 1; + color: var(--ifm-color-emphasis-800); +} + +[data-theme='dark'] .project-description { + color: var(--ifm-color-emphasis-600); +} + +.project-links { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: auto; +} + +.project-link { + padding: 11px 20px; + border-radius: 10px; + text-align: center; + font-weight: 600; + font-size: 0.9rem; + text-decoration: none; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + line-height: 1; + vertical-align: middle; + white-space: nowrap; + position: relative; + overflow: hidden; + width: 100%; +} + +.project-link svg { + transition: transform 0.3s ease; +} + +.project-link:hover svg { + transform: scale(1.1); +} + +.project-link-primary { + background: linear-gradient(135deg, var(--teal-6) 0%, var(--teal-7) 100%); + color: white; + box-shadow: 0 3px 10px rgba(4, 159, 154, 0.25); + border: 1px solid transparent; +} + +.project-link-primary:hover { + background: linear-gradient(135deg, var(--teal-7) 0%, var(--teal-10) 100%); + color: white; + box-shadow: 0 6px 16px rgba(4, 159, 154, 0.35); + transform: translateY(-2px); +} + +.project-link-primary:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(4, 159, 154, 0.3); +} + +.project-link-secondary { + background: var(--ifm-color-emphasis-0); + color: var(--teal-7); + border: 2px solid var(--teal-6); + box-shadow: 0 2px 6px rgba(4, 159, 154, 0.1); +} + +.project-link-secondary:hover { + background: var(--teal-6); + color: white; + border-color: var(--teal-6); + box-shadow: 0 6px 16px rgba(4, 159, 154, 0.25); + transform: translateY(-2px); +} + +.project-link-secondary:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(4, 159, 154, 0.2); +} + +[data-theme='dark'] .project-link-primary { + background: linear-gradient(135deg, var(--teal-6) 0%, var(--teal-7) 100%); + box-shadow: 0 3px 10px rgba(44, 224, 191, 0.2); +} + +[data-theme='dark'] .project-link-primary:hover { + background: linear-gradient(135deg, var(--teal-4) 0%, var(--teal-6) 100%); + box-shadow: 0 6px 16px rgba(44, 224, 191, 0.3); +} + +[data-theme='dark'] .project-link-secondary { + background: rgba(255, 255, 255, 0.03); + color: var(--teal-4); + border-color: var(--teal-6); +} + +[data-theme='dark'] .project-link-secondary:hover { + background: var(--teal-6); + color: var(--ifm-color-emphasis-0); +} + +@media (max-width: 768px) { + .ecosystem-grid { + grid-template-columns: 1fr; + } +} + +/* ===== Private/Grayed Out Providers ===== */ +.project-card.project-card-private { + opacity: 0.5; + filter: grayscale(0.7); + pointer-events: none; +} + +.project-card.project-card-private:hover { + transform: none; + box-shadow: none; + border-color: var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .project-card.project-card-private:hover { + border-color: rgba(255, 255, 255, 0.08); + box-shadow: none; +} + +/* ===== CTA Card (e.g., Build Your Own) ===== */ +.project-card.project-card-cta { + border: 2px dashed var(--teal-6); + background: linear-gradient(135deg, rgba(4, 159, 154, 0.03) 0%, rgba(44, 224, 191, 0.03) 100%); +} + +.project-card.project-card-cta:hover { + border-color: var(--teal-4); + background: linear-gradient(135deg, rgba(4, 159, 154, 0.06) 0%, rgba(44, 224, 191, 0.06) 100%); +} + +[data-theme='dark'] .project-card.project-card-cta { + border-color: var(--teal-6); + background: linear-gradient(135deg, rgba(4, 159, 154, 0.05) 0%, rgba(44, 224, 191, 0.05) 100%); +} + +[data-theme='dark'] .project-card.project-card-cta:hover { + border-color: var(--teal-4); + background: linear-gradient(135deg, rgba(4, 159, 154, 0.08) 0%, rgba(44, 224, 191, 0.08) 100%); +} + +.project-card.project-card-cta svg { + color: var(--teal-6); + opacity: 0.8; +} + +.project-card.project-card-cta:hover svg { + opacity: 1; + color: var(--teal-4); +} + +.project-card.project-card-cta h3 { + background: linear-gradient(135deg, var(--teal-6) 0%, var(--teal-4) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* SVG icons in project card headers */ +.project-card-header svg.project-logo { + width: 48px !important; + height: 48px !important; + min-width: 48px !important; + min-height: 48px !important; + max-width: 48px !important; + max-height: 48px !important; + color: var(--teal-6); + flex-shrink: 0 !important; +} + +[data-theme='dark'] .project-card-header svg.project-logo { + color: var(--teal-4); +} + +/* ===== Benefits List ===== */ +.benefits-list { + margin: 24px 0; + padding: 0; +} + +.benefits-list ul { + list-style: none; + padding: 0; + margin: 0; +} + +.benefits-list li { + padding: 8px 0; + font-size: 1rem; + line-height: 1.6; + color: var(--ifm-color-emphasis-800); +} + +[data-theme='dark'] .benefits-list li { + color: var(--ifm-color-emphasis-600); +} + +/* ===== CRD Viewer Styles ===== */ +.crd-viewer { + margin: 32px 0; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 12px; + overflow: hidden; +} + +[data-theme='dark'] .crd-viewer { + border-color: rgba(255, 255, 255, 0.1); +} + +.crd-header { + background: linear-gradient(135deg, rgba(4, 159, 154, 0.05) 0%, rgba(44, 224, 191, 0.05) 100%); + padding: 24px; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .crd-header { + background: linear-gradient(135deg, rgba(4, 159, 154, 0.08) 0%, rgba(44, 224, 191, 0.08) 100%); + border-bottom-color: rgba(255, 255, 255, 0.1); +} + +.crd-header h3 { + margin: 0 0 8px 0; + font-size: 1.5rem; + color: var(--teal-7); +} + +[data-theme='dark'] .crd-header h3 { + color: var(--teal-4); +} + +.crd-description { + margin: 0; + color: var(--ifm-color-emphasis-700); + font-size: 0.95rem; +} + +[data-theme='dark'] .crd-description { + color: var(--ifm-color-emphasis-600); +} + +.crd-schema { + padding: 24px; +} + +.crd-properties-table { + width: 100%; + border-collapse: collapse; + margin: 0; +} + +.crd-properties-table th { + text-align: left; + padding: 12px; + background: var(--ifm-color-emphasis-100); + font-weight: 600; + border-bottom: 2px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .crd-properties-table th { + background: rgba(255, 255, 255, 0.05); + border-bottom-color: rgba(255, 255, 255, 0.1); +} + +.crd-properties-table td { + padding: 12px; + border-bottom: 1px solid var(--ifm-color-emphasis-100); + vertical-align: top; +} + +[data-theme='dark'] .crd-properties-table td { + border-bottom-color: rgba(255, 255, 255, 0.05); +} + +.crd-properties-table td:first-child { + font-weight: 500; +} + +.crd-type-badge { + display: inline-block; + padding: 2px 8px; + background: var(--teal-6); + color: white; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 600; +} + +[data-theme='dark'] .crd-type-badge { + background: var(--teal-7); +} + +.crd-required-badge { + display: inline-block; + padding: 2px 8px; + background: var(--ifm-color-warning); + color: var(--ifm-color-emphasis-0); + border-radius: 4px; + font-size: 0.85rem; + font-weight: 600; +} + +[data-theme='dark'] .crd-required-badge { + background: var(--ifm-color-warning-dark); +} + +.crd-loading, +.crd-error { + padding: 24px; + text-align: center; + color: var(--ifm-color-emphasis-600); +} + +.crd-error { + color: var(--ifm-color-danger); +} + +/* ===== Compact CRD Viewer with Tree Structure ===== */ +.crd-viewer-compact { + margin: 24px 0; +} + +/* Hide table of contents on reference/CRD pages */ +body[class*="docs-doc-id-reference"] .col--3:last-child { + display: none !important; +} + +body[class*="docs-doc-id-reference"] .col--9 { + --ifm-col-width: 100% !important; + flex-basis: 100% !important; + max-width: 100% !important; +} + +body[class*="docs-doc-id-reference"] .docMainContainer_TBSr .container { + max-width: 100% !important; + padding: 0 40px !important; +} + +body[class*="docs-doc-id-reference"] article { + max-width: 100% !important; +} + +body[class*="docs-doc-id-reference"] .container { + max-width: 100% !important; +} + +body[class*="docs-doc-id-reference"] .row { + max-width: 100% !important; +} + +/* CRD Header with Icon */ +.crd-header-container { + display: flex; + align-items: center; + gap: 24px; + margin: 24px 0 32px 0; + padding: 24px; + background: linear-gradient(135deg, rgba(4, 159, 154, 0.03) 0%, rgba(10, 125, 121, 0.05) 100%); + border-radius: 16px; + border: 1px solid rgba(4, 159, 154, 0.15); +} + +[data-theme='dark'] .crd-header-container { + background: linear-gradient(135deg, rgba(4, 159, 154, 0.08) 0%, rgba(10, 125, 121, 0.12) 100%); + border-color: rgba(4, 159, 154, 0.25); +} + +.crd-header-icon { + width: 80px; + height: 80px; + flex-shrink: 0; + filter: drop-shadow(0 4px 12px rgba(4, 159, 154, 0.3)); +} + +.crd-header-icon-custom { + width: 220px; + height: 120px; + flex-shrink: 0; + position: relative; + display: flex; + align-items: center; + justify-content: center; + filter: drop-shadow(0 4px 12px rgba(4, 159, 154, 0.3)); +} + +/* Workspace icon in CRD header */ +.crd-header-icon-custom .reference-icon-group { + width: 160px; + height: 120px; + border-radius: 50%; + border: 4px solid var(--teal-7); + background: transparent; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +[data-theme='dark'] .crd-header-icon-custom .reference-icon-group { + border-color: var(--teal-6); +} + +.crd-header-icon-custom .reference-icon-group .reference-icon-medium { + width: 50px !important; + height: 50px !important; +} + +.crd-header-icon-custom .reference-icon-group .reference-icon-left { + left: 15px; + top: 50%; + transform: translateY(-50%); +} + +.crd-header-icon-custom .reference-icon-group .reference-icon-right { + right: 15px; + top: 50%; + transform: translateY(-50%); +} + +.crd-header-icon-custom .reference-icon-label { + font-size: 11px; + margin-top: 8px; +} + +/* Project icon in CRD header */ +.crd-header-icon-project { + flex-shrink: 0; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + filter: drop-shadow(0 4px 12px rgba(4, 159, 154, 0.2)); +} + +.crd-header-icon-project .reference-icon-label { + margin-top: 0; + font-family: var(--ifm-font-family-base); + font-size: 13px; +} + +/* Project icon in CRD header */ +.crd-header-icon-project { + flex-shrink: 0; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + filter: drop-shadow(0 4px 12px rgba(4, 159, 154, 0.2)); +} + +.crd-header-icon-project .reference-icon-label { + margin-top: 0; + font-family: var(--ifm-font-family-base); + font-size: 13px; +} + +.reference-icon-project-label { + font-size: 14px; + font-weight: 600; + color: var(--teal-7); + text-align: center; + font-family: var(--ifm-font-family-base); + margin-top: 4px; +} + +[data-theme='dark'] .reference-icon-project-label { + color: var(--teal-5); +} + +.crd-header-text { + flex: 1; +} + +.crd-header-text p { + margin: 0; + font-size: 1.05rem; + line-height: 1.6; + color: var(--ifm-color-emphasis-800); +} + +[data-theme='dark'] .crd-header-text p { + color: var(--ifm-color-emphasis-700); +} + +.schema-search-container { + padding: 20px; + background: linear-gradient(135deg, rgba(4, 159, 154, 0.03) 0%, rgba(10, 125, 121, 0.05) 100%); + border-bottom: 1px solid rgba(4, 159, 154, 0.15); + backdrop-filter: blur(10px); +} + +[data-theme='dark'] .schema-search-container { + background: linear-gradient(135deg, rgba(4, 159, 154, 0.08) 0%, rgba(10, 125, 121, 0.12) 100%); + border-bottom-color: rgba(4, 159, 154, 0.25); +} + +.schema-search-wrapper { + position: relative; + max-width: 600px; + margin: 0 auto; +} + +.schema-search-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--teal-6); + opacity: 0.7; + pointer-events: none; +} + +[data-theme='dark'] .schema-search-icon { + color: var(--teal-5); +} + +.schema-search-input { + width: 100%; + padding: 12px 16px 12px 44px; + font-size: 0.95rem; + border: 1px solid rgba(4, 159, 154, 0.25); + border-radius: 12px; + background: white; + color: #1c1e21; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 8px rgba(4, 159, 154, 0.08); +} + +.schema-search-input:focus { + outline: none; + border-color: var(--teal-6); + box-shadow: 0 4px 16px rgba(4, 159, 154, 0.2); + transform: translateY(-1px); +} + +[data-theme='dark'] .schema-search-input { + background: rgba(255, 255, 255, 0.95); + color: #1c1e21; + border-color: rgba(4, 159, 154, 0.3); +} + +.schema-tree { + padding: 24px 20px; +} + +.schema-field { + margin: 4px 0; + padding: 12px 16px; + border-left: 3px solid transparent; + border-radius: 8px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: transparent; +} + +.schema-field:hover { + background: linear-gradient(90deg, rgba(4, 159, 154, 0.05) 0%, transparent 100%); + border-left-color: var(--teal-6); + transform: translateX(2px); + box-shadow: 0 2px 8px rgba(4, 159, 154, 0.08); +} + +[data-theme='dark'] .schema-field:hover { + background: linear-gradient(90deg, rgba(4, 159, 154, 0.12) 0%, transparent 100%); +} + +.schema-field-depth-1 { + margin-left: 24px; +} + +.schema-field-depth-2 { + margin-left: 48px; +} + +.schema-field-depth-3 { + margin-left: 72px; +} + +.schema-field-depth-4 { + margin-left: 96px; +} + +.schema-field-header { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; +} + +.schema-field-toggle { + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + color: var(--ifm-color-emphasis-600); + transition: transform 0.2s ease; +} + +.schema-field-toggle.expanded { + transform: rotate(90deg); +} + +.schema-field-name { + font-weight: 600; + font-size: 0.95rem; + color: var(--ifm-font-color-base); +} + +.schema-field-type { + font-size: 0.8rem; + padding: 4px 10px; + background: linear-gradient(135deg, var(--teal-6) 0%, var(--teal-7) 100%); + color: white; + border-radius: 6px; + font-weight: 600; + box-shadow: 0 2px 6px rgba(4, 159, 154, 0.25); + text-transform: lowercase; + letter-spacing: 0.3px; +} + +[data-theme='dark'] .schema-field-type { + background: linear-gradient(135deg, var(--teal-7) 0%, var(--teal-8) 100%); + box-shadow: 0 2px 6px rgba(4, 159, 154, 0.4); +} + +.schema-field-required { + font-size: 0.75rem; + padding: 2px 6px; + background: rgba(255, 0, 0, 0.12); + color: var(--ifm-color-danger); + border: 1px solid rgba(255, 0, 0, 0.3); + border-radius: 3px; + font-weight: 600; + text-transform: uppercase; +} + +[data-theme='dark'] .schema-field-required { + background: rgba(255, 0, 0, 0.15); + color: #ff6b6b; + border-color: rgba(255, 0, 0, 0.4); +} + + +.schema-field-description { + margin: 8px 0 0 24px; + font-size: 0.9rem; + color: var(--ifm-color-emphasis-700); + line-height: 1.5; +} + +[data-theme='dark'] .schema-field-description { + color: var(--ifm-color-emphasis-600); +} + +.schema-field-children { + margin-top: 8px; +} + +/* ===== Reference Grid ===== */ +.reference-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin: 24px 0; +} + +.reference-card { + background: var(--ifm-card-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 12px; + padding: 24px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +/* Compact variant for community action cards */ +.reference-card-compact { + padding: 20px; +} + +.reference-card-compact .reference-icon-container { + min-height: auto; + margin-bottom: 12px; +} + +.reference-card-compact h3 { + margin-bottom: 8px; + font-size: 1.1rem; +} + +.reference-card-compact p { + margin-bottom: 12px; + font-size: 0.9rem; +} + +/* Featured/Hero card with gradient - Simplified */ +.reference-card-featured { + background: linear-gradient(135deg, + #049f9a 0%, + #0a7d79 50%, + #064d4b 100% + ); + border: none; + padding: 40px; + color: white; + position: relative; + overflow: hidden; + box-shadow: 0 20px 60px rgba(4, 159, 154, 0.4); +} + +.reference-card-featured::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at top right, + rgba(255, 255, 255, 0.1) 0%, + transparent 60% + ); + pointer-events: none; +} + +.reference-card-featured * { + position: relative; + z-index: 2; +} + +.reference-card-featured .reference-icon-container > div { + color: white !important; + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.4)); +} + +.reference-card-featured h3 { + color: white !important; + font-size: 2.2rem; + font-weight: 800; + margin-bottom: 16px; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + letter-spacing: -0.5px; +} + +.reference-card-featured p { + color: white !important; + font-size: 1.15rem; + line-height: 1.7; + font-weight: 400; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +.reference-card-featured .reference-icon-container { + margin-bottom: 24px; +} + +.sig-details { + display: flex; + flex-direction: column; + gap: 8px; + margin: 20px 0 24px 0; + padding: 0; + background: transparent; + backdrop-filter: none; + border-radius: 0; + border: none; + box-shadow: none; +} + +.sig-details div { + font-size: 0.95rem; + line-height: 1.5; + color: white !important; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + font-weight: 400; +} + +.sig-details strong { + color: white !important; + font-weight: 600; + margin-right: 6px; +} + +.reference-card-featured a.reference-link { + color: white !important; + border: none; + background: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + font-weight: 600; + padding: 10px 20px; + border-radius: 8px; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.9rem; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + text-decoration: none; +} + +.reference-card-featured a.reference-link:hover { + background: rgba(0, 0, 0, 0.3); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.reference-card-featured:hover { + transform: translateY(-4px); + box-shadow: 0 24px 80px rgba(4, 159, 154, 0.5); +} + +[data-theme='dark'] .reference-card-featured { + background: linear-gradient(135deg, + #0a7d79 0%, + #064d4b 50%, + #032423 100% + ); + box-shadow: 0 20px 60px rgba(44, 224, 191, 0.4); +} + +[data-theme='dark'] .reference-card-featured:hover { + box-shadow: 0 24px 80px rgba(44, 224, 191, 0.5); +} + +[data-theme='dark'] .reference-card { + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.08); +} + +.reference-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(4, 159, 154, 0.15); + border-color: var(--teal-4); +} + +[data-theme='dark'] .reference-card:hover { + box-shadow: 0 8px 20px rgba(44, 224, 191, 0.15); +} + +.reference-icon { + width: 80px !important; + height: 80px !important; + object-fit: contain !important; + margin-bottom: 16px; + filter: brightness(0.95); +} + +[data-theme='dark'] .reference-icon { + filter: brightness(1.1); +} + +.reference-icon-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 16px; + min-height: 140px; + justify-content: flex-start; +} + +.reference-icon-container-standard { + justify-content: center; +} + +/* Compact variant for community pages */ +.reference-icon-container-compact { + min-height: auto; + margin-bottom: 12px; +} + +/* Large icons for simple resources (MCP, ServiceProvider, etc.) */ +.reference-icon-large { + width: 120px !important; + height: 120px !important; + object-fit: contain !important; + filter: brightness(0.95); +} + +[data-theme='dark'] .reference-icon-large { + filter: brightness(1.1); +} + +/* Project container with two workspaces */ +.reference-icon-project-container { + display: flex; + gap: 20px; + align-items: flex-end; + margin-bottom: 8px; + padding: 12px 16px; + background: rgba(4, 159, 154, 0.08); + border: 3px solid rgba(4, 159, 154, 0.4); + border-radius: 12px; +} + +[data-theme='dark'] .reference-icon-project-container { + background: rgba(4, 159, 154, 0.12); + border-color: rgba(4, 159, 154, 0.5); +} + +.reference-icon-workspace-mini { + width: 80px; + height: 80px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + border: 3px solid; + background: transparent; +} + +.reference-icon-workspace-dev { + border-color: var(--teal-6); +} + +[data-theme='dark'] .reference-icon-workspace-dev { + border-color: var(--teal-4); +} + +.reference-icon-workspace-prod { + border-color: #049F9A; +} + +[data-theme='dark'] .reference-icon-workspace-prod { + border-color: #2CE0BF; +} + +.reference-icon-label-mini { + position: absolute; + bottom: -18px; + left: 50%; + transform: translateX(-50%); + font-family: var(--ifm-font-family-monospace); + font-size: 0.7rem; + color: var(--ifm-color-emphasis-700); + font-weight: 600; + white-space: nowrap; +} + +[data-theme='dark'] .reference-icon-label-mini { + color: var(--ifm-color-emphasis-600); +} + +.reference-icon-small { + width: 30px !important; + height: 30px !important; + object-fit: contain !important; + position: absolute; + filter: brightness(0.9); + opacity: 0.85; +} + +[data-theme='dark'] .reference-icon-small { + filter: brightness(1.2); + opacity: 0.9; +} + +/* Side-by-side positioning for small CPs in mini workspaces */ +.reference-icon-workspace-mini .reference-icon-left { + top: 50%; + left: 10px; + transform: translateY(-50%); +} + +.reference-icon-workspace-mini .reference-icon-right { + top: 50%; + right: 10px; + transform: translateY(-50%); +} + +/* Single workspace group */ +.reference-icon-group { + width: 160px; + height: 120px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + border: 4px solid; + background: transparent; +} + +.reference-icon-group-workspace { + border-color: var(--teal-7); +} + +[data-theme='dark'] .reference-icon-group-workspace { + border-color: var(--teal-6); +} + +.reference-icon-medium { + width: 50px !important; + height: 50px !important; + object-fit: contain !important; + position: absolute; + filter: brightness(0.9); + opacity: 0.85; +} + +[data-theme='dark'] .reference-icon-medium { + filter: brightness(1.2); + opacity: 0.9; +} + +/* Side-by-side positioning for medium CPs */ +.reference-icon-group .reference-icon-left { + top: 50%; + left: 15px; + transform: translateY(-50%); +} + +.reference-icon-group .reference-icon-right { + top: 50%; + right: 15px; + transform: translateY(-50%); +} + +.reference-icon-label { + margin-top: 8px; + font-family: var(--ifm-font-family-monospace); + font-size: 0.8rem; + color: var(--ifm-color-emphasis-700); + font-weight: 600; + letter-spacing: 0.5px; +} + +[data-theme='dark'] .reference-icon-label { + color: var(--ifm-color-emphasis-600); +} + +.reference-card:hover .reference-icon-group, +.reference-card:hover .reference-icon-workspace-mini, +.reference-card:hover .reference-icon-large { + transform: scale(1.05); + transition: transform 0.2s ease; +} + +.reference-card:hover .reference-icon-label, +.reference-card:hover .reference-icon-label-mini { + color: var(--teal-6); + transition: color 0.2s ease; +} + +[data-theme='dark'] .reference-card:hover .reference-icon-label, +[data-theme='dark'] .reference-card:hover .reference-icon-label-mini { + color: var(--teal-4); +} + + + + +.reference-card h3 { + margin: 0 0 12px 0; + font-size: 1.25rem; + color: var(--teal-7); +} + +[data-theme='dark'] .reference-card h3 { + color: var(--teal-4); +} + +.reference-card p { + margin: 0 0 16px 0; + font-size: 0.9rem; + color: var(--ifm-color-emphasis-700); + line-height: 1.5; + flex-grow: 1; +} + +[data-theme='dark'] .reference-card p { + color: var(--ifm-color-emphasis-600); +} + +.reference-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--teal-7); + text-decoration: none; + font-weight: 600; + font-size: 0.9rem; + transition: gap 0.2s ease; +} + +.reference-link:hover { + gap: 10px; + color: var(--teal-6); +} + +[data-theme='dark'] .reference-link { + color: var(--teal-4); +} + +[data-theme='dark'] .reference-link:hover { + color: var(--teal-2); +} + + + + + + + + +/* ===== Role-based Page Header Accents ===== */ +/* Add subtle top border to main content based on current section */ +article[data-role="enduser"] { + border-top: 4px solid var(--role-enduser-primary); + padding-top: 1rem; +} + +/* ===== CRITICAL: Force project logo sizes ===== */ +/* This must be at the end to override Docusaurus defaults */ +.ecosystem-grid .project-card-header img.project-logo, +.ecosystem-grid img[class*="project-logo"], +.project-card-header img[alt="Kubernetes"], +.project-card-header img[alt="Crossplane"], +.project-card-header img[alt="Gardener"], +.project-card-header img[alt="Flux"], +.project-card-header img[alt="Kyverno"], +.project-card-header img[alt="External Secrets"], +.project-card-header img[alt="Open Component Model"], +.project-card-header img[alt="Landscaper"] { + width: 64px !important; + height: 64px !important; + min-width: 64px !important; + min-height: 64px !important; + max-width: 64px !important; + max-height: 64px !important; + object-fit: contain !important; + flex-shrink: 0 !important; + display: block !important; +} + +/* ===== Axolotl Hidden - Control Planes Only ===== */ +.image-src { + opacity: 0; + pointer-events: none; + display: none; +} + +/* ===== Flying Control Planes ===== */ +.flying-cp { + position: absolute; + width: 180px; + height: 180px; + object-fit: contain; + z-index: 2; + opacity: 0; + filter: drop-shadow(0 2px 8px rgba(4, 159, 154, 0.3)); + transform: translateY(100vh); + transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.8s ease-out; +} + +.flying-cp.visible { + opacity: 0.85; + transform: translateY(0); +} + +[data-theme='dark'] .flying-cp { + filter: drop-shadow(0 2px 12px rgba(44, 224, 191, 0.4)); +} + +@keyframes float-cp-1 { + 0%, 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(8px, -10px); + } +} + +@keyframes float-cp-2 { + 0%, 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(-10px, -8px); + } +} + +@keyframes float-cp-3 { + 0%, 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(10px, -12px); + } +} + +.flying-cp-1 { + bottom: -8%; + left: 1%; + transition-delay: 0s; +} + +.flying-cp-1.visible { + animation: float-cp-1 5s ease-in-out 0.8s infinite; +} + +.flying-cp-2 { + bottom: 30%; + left: 35%; + transform: translateY(100vh); + transition-delay: 0.2s; +} + +.flying-cp-2.visible { + transform: translateY(0); + animation: float-cp-2 5.5s ease-in-out 1s infinite; +} + +.flying-cp-3 { + bottom: -5%; + right: 1%; + transition-delay: 0.4s; +} + +.flying-cp-3.visible { + animation: float-cp-3 5.2s ease-in-out 1.2s infinite; +} + +/* ===== Cloud Projection - Cutting Edge Animation ===== */ +.cp-cloud-projection { + position: absolute; + width: 364.32px; + height: 255.024px; + z-index: 10; + opacity: 0; + pointer-events: none; + transform: translateY(50px); + transition: opacity 0.8s ease-out, transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.cp-cloud-projection.visible { + opacity: 1; + transform: translateY(0); +} + +.cp-cloud-1 { + bottom: 14%; + left: -18%; + transition-delay: 0.8s; +} + +.cp-cloud-1.visible { + animation: cloud-float-gentle 4s ease-in-out 1.6s infinite; +} + +.cp-cloud-2-1 { + bottom: 47%; + left: 7%; + transition-delay: 1s; +} + +.cp-cloud-2-1.visible { + animation: cloud-float-gentle 4.2s ease-in-out 1.8s infinite; +} + +.cp-cloud-2-2 { + bottom: 47%; + left: 28%; + transition-delay: 1.2s; +} + +.cp-cloud-2-2.visible { + animation: cloud-float-gentle 4.4s ease-in-out 2s infinite; +} + +.cp-cloud-3 { + bottom: 17%; + right: -23%; + transition-delay: 1.4s; +} + +.cp-cloud-3.visible { + animation: cloud-float-gentle 4.1s ease-in-out 2.2s infinite; +} + +@keyframes cloud-float-gentle { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-8px); + } +} + +.cp-cloud-projection { + animation: cloud-projection-appear 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) 1s forwards, + cloud-float-gentle 4s ease-in-out 2.2s infinite; +} + +/* Connection line pulse */ +.cloud-connection { + animation: connection-pulse 3s ease-in-out infinite; +} + +@keyframes connection-pulse { + 0%, 100% { + opacity: 0.45; + stroke-width: 1; + } + 50% { + opacity: 0.75; + stroke-width: 1.5; + } +} + +/* Main hexagon breathing */ +.cloud-hex-1 { + transform-origin: center; + animation: hex-breathe 4s ease-in-out infinite; +} + +@keyframes hex-breathe { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.05); + opacity: 0.9; + } +} + +/* Center dot pulse */ +.cloud-dot-1 { + animation: dot-pulse 2s ease-in-out infinite; +} + +@keyframes dot-pulse { + 0%, 100% { + opacity: 0.4; + r: 2; + } + 50% { + opacity: 0.8; + r: 2.5; + } +} + +/* Side dots subtle fade */ +.cloud-dot-2, .cloud-dot-3 { + animation: dot-fade 3s ease-in-out infinite; +} + +@keyframes dot-fade { + 0%, 100% { + opacity: 0.3; + } + 50% { + opacity: 0.6; + } +} + +/* Sonar sweep animation */ +.sonar-sweep { + animation: sonar-rotate 8s linear infinite; + transform-origin: 60px 25px; +} + +@keyframes sonar-rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Resource icons sparkle animations */ +.cloud-resource-icon { + animation: icon-sparkle 3s ease-in-out infinite; +} + +.icon-user { + animation-delay: 0s; +} + +.icon-database { + animation-delay: 0.4s; +} + +.icon-key { + animation-delay: 0.8s; +} + +.icon-cpu { + animation-delay: 1.2s; +} + +.icon-docker { + animation-delay: 1.6s; +} + +.icon-server { + animation-delay: 2s; +} + +.icon-network { + animation-delay: 2.4s; +} + +.icon-harddrive { + animation-delay: 0.6s; +} + +.icon-settings { + animation-delay: 1s; +} + +.icon-shield { + animation-delay: 1.4s; +} + +.icon-lock { + animation-delay: 1.8s; +} + +.icon-memory { + animation-delay: 2.2s; +} + +.icon-globe { + animation-delay: 2.6s; +} + +@keyframes icon-sparkle { + 0%, 100% { + opacity: 0.4; + } + 50% { + opacity: 0.85; + } +} + +/* Dark mode adjustments */ +[data-theme='dark'] .cloud-connection { + stroke: rgba(44, 224, 191, 0.3); +} + +[data-theme='dark'] .cloud-hex-1 { + stroke: rgba(44, 224, 191, 0.4); +} + +[data-theme='dark'] .cloud-dot-1, +[data-theme='dark'] .cloud-particle-1, +[data-theme='dark'] .cloud-particle-2, +[data-theme='dark'] .cloud-particle-3 { + fill: rgba(44, 224, 191, 0.6); +} + +[data-theme='dark'] .cloud-badge text { + fill: #ffffff; +} + +[data-theme='dark'] .sonar-sweep circle { + fill: rgba(44, 224, 191, 0.7); +} + +[data-theme='dark'] .sonar-sweep circle:nth-child(2) { + fill: rgba(44, 224, 191, 0.5); +} + +[data-theme='dark'] .sonar-sweep circle:nth-child(3) { + fill: rgba(44, 224, 191, 0.4); +} + +[data-theme='dark'] .sonar-sweep circle:nth-child(4) { + fill: rgba(44, 224, 191, 0.3); +} + +[data-theme='dark'] .sonar-sweep circle:nth-child(5) { + fill: rgba(44, 224, 191, 0.2); +} + +[data-theme='dark'] .cloud-dot-2, +[data-theme='dark'] .cloud-dot-3 { + fill: rgba(194, 252, 238, 0.4); +} + +/* Responsive */ +@media (max-width: 996px) { + .cp-cloud-projection { + width: 227.7px; + height: 159.39px; + } +} + +@media (max-width: 768px) { + .cp-cloud-projection { + width: 303.6px; + height: 212.52px; + } + + .flying-cp { + width: 150px; + height: 150px; + } + + /* Axolotl fades out on mobile too */ + .image-src { + opacity: 1; + transition: opacity 0.6s ease-out; + } + + .image-src.scrolled { + opacity: 0; + } + + /* Hide CP3 and its cloud on mobile */ + .flying-cp-3, + .cp-cloud-3 { + display: none !important; + } + + /* CP1 centered below Cloud 1 */ + .flying-cp-1 { + bottom: -4%; + left: 15%; + } + + /* CP2 centered below the two clouds */ + .flying-cp-2 { + bottom: 20%; + right: 15%; + left: auto; + } + + .flying-cp-2.visible { + transform: translateY(0); + } + + /* Cloud 1 above CP1 */ + .cp-cloud-1 { + bottom: 12%; + left: 2%; + } + + /* Cloud 2-1 (left cloud above CP2) */ + .cp-cloud-2-1 { + bottom: 38%; + right: 6%; + left: auto; + } + + /* Cloud 2-2 (right cloud above CP2) */ + .cp-cloud-2-2 { + bottom: 38%; + right: -8%; + left: auto; + } +} + +/* ===== Essentials Section ===== */ +.essentials-section { + padding: 80px 20px 0px; + background: linear-gradient(135deg, rgba(194, 252, 238, 0.15) 0%, rgba(44, 224, 191, 0.08) 50%, rgba(4, 159, 154, 0.05) 100%); + position: relative; + overflow: hidden; +} + +[data-theme='dark'] .essentials-section { + background: linear-gradient(135deg, rgba(44, 224, 191, 0.08) 0%, rgba(4, 159, 154, 0.05) 50%, rgba(2, 65, 76, 0.1) 100%); +} + +.essentials-container { + max-width: 1152px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 60px; +} + +.essentials-title { + font-size: 2rem; + font-weight: 700; + color: var(--teal-6); + margin-bottom: 12px; + line-height: 1.3; +} + +.section-cta-link { + font-size: 0.95rem; + color: var(--teal-6); + text-decoration: none; + margin-bottom: 16px; + display: inline-block; + transition: color 0.2s ease; +} + +.section-cta-link:hover { + color: var(--teal-5); + text-decoration: underline; +} + +[data-theme='dark'] .section-cta-link { + color: #2dd4bf; +} + +[data-theme='dark'] .section-cta-link:hover { + color: #5eead4; +} + +.essentials-card { + display: flex; + gap: 40px; + align-items: flex-start; + padding: 50px; + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(10px); + border-radius: 24px; + border: 1px solid rgba(4, 159, 154, 0.15); + position: relative; + opacity: 1; + transform: translateY(0); + box-shadow: 0 8px 32px rgba(4, 159, 154, 0.08); +} + +.essentials-card.visible { + opacity: 1; + transform: translateY(0); +} + +[data-theme='dark'] .essentials-card { + background: rgba(32, 33, 39, 0.7); + border: 1px solid rgba(44, 224, 191, 0.2); + box-shadow: 0 8px 32px rgba(44, 224, 191, 0.12); +} + +.essentials-card::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 24px; + padding: 2px; + background: linear-gradient(135deg, rgba(4, 159, 154, 0.3), rgba(44, 224, 191, 0.1)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 0.4s ease; +} + +.essentials-card:hover::before { + opacity: 1; +} + +.essentials-card-number { + font-size: 5rem; + font-weight: 900; + line-height: 1; + color: var(--teal-6); + opacity: 0.15; + flex-shrink: 0; + font-family: ui-monospace, monospace; +} + +[data-theme='dark'] .essentials-card-number { + color: var(--teal-4); + opacity: 0.2; +} + +.essentials-card-content { + flex: 1; +} + +.essentials-card-content h3 { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 24px; + color: var(--teal-10); + line-height: 1.3; +} + +[data-theme='dark'] .essentials-card-content h3 { + color: var(--teal-2); +} + +.essentials-features { + display: flex; + flex-direction: column; + gap: 16px; +} + +.essentials-features-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.essentials-feature { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 18px; + background: rgba(4, 159, 154, 0.05); + border-radius: 12px; + border: 1px solid rgba(4, 159, 154, 0.1); + transition: all 0.3s ease; + font-size: 1rem; + font-weight: 500; + color: var(--teal-10); +} + +[data-theme='dark'] .essentials-feature { + background: rgba(44, 224, 191, 0.08); + border: 1px solid rgba(44, 224, 191, 0.15); + color: var(--teal-2); +} + +.essentials-feature:hover { + transform: translateX(4px); + background: rgba(4, 159, 154, 0.12); + border-color: rgba(4, 159, 154, 0.3); +} + +[data-theme='dark'] .essentials-feature:hover { + background: rgba(44, 224, 191, 0.15); + border-color: rgba(44, 224, 191, 0.35); +} + +.essentials-feature svg { + flex-shrink: 0; + color: var(--teal-6); +} + +[data-theme='dark'] .essentials-feature svg { + color: var(--teal-4); +} + +.essentials-feature span { + line-height: 1.4; +} + +.essentials-feature-new { + position: relative; +} + +.new-badge { + display: inline-block; + padding: 2px 8px; + background: linear-gradient(135deg, var(--teal-4) 0%, var(--teal-6) 100%); + color: white; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 8px; + vertical-align: middle; +} + +@media (max-width: 996px) { + .essentials-section { + padding: 80px 20px 60px; + } + + .essentials-card { + padding: 40px 30px; + gap: 30px; + } + + .essentials-card-number { + font-size: 4rem; + } + + .essentials-card-content h3 { + font-size: 1.5rem; + } + + .essentials-features-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .essentials-section { + padding: 60px 16px 40px; + } + + .essentials-container { + gap: 40px; + } + + .essentials-title { + font-size: 2.2rem; + } + + .essentials-card { + flex-direction: column; + padding: 30px 24px; + gap: 20px; + } + + .essentials-card-number { + font-size: 3rem; + align-self: flex-start; + } + + .essentials-card-content h3 { + font-size: 1.4rem; + margin-bottom: 20px; + } + + .essentials-feature { + font-size: 0.95rem; + padding: 12px 16px; + } + + .essentials-feature svg { + width: 18px; + height: 18px; + } +} + +/* ===== Essentials Section ===== */ +.essentials-section { + padding: 80px 20px 0px; + background: linear-gradient(135deg, rgba(194, 252, 238, 0.15) 0%, rgba(44, 224, 191, 0.08) 50%, rgba(4, 159, 154, 0.05) 100%); + position: relative; + overflow: hidden; +} + +[data-theme='dark'] .essentials-section { + background: linear-gradient(135deg, rgba(44, 224, 191, 0.08) 0%, rgba(4, 159, 154, 0.05) 50%, rgba(2, 65, 76, 0.1) 100%); +} + +.essentials-container { + max-width: 1152px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 60px; +} + +.essentials-title { + font-size: 2rem; + font-weight: 700; + color: var(--teal-6); + margin-bottom: 12px; + line-height: 1.3; +} + +.section-cta-link { + font-size: 0.95rem; + color: var(--teal-6); + text-decoration: none; + margin-bottom: 16px; + display: inline-block; + transition: color 0.2s ease; +} + +.section-cta-link:hover { + color: var(--teal-5); + text-decoration: underline; +} + +[data-theme='dark'] .section-cta-link { + color: #2dd4bf; +} + +[data-theme='dark'] .section-cta-link:hover { + color: #5eead4; +} + +.essentials-card { + display: flex; + gap: 40px; + align-items: flex-start; + padding: 50px; + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(10px); + border-radius: 24px; + border: 1px solid rgba(4, 159, 154, 0.15); + position: relative; + opacity: 1; + transform: translateY(0); + box-shadow: 0 8px 32px rgba(4, 159, 154, 0.08); +} + +.essentials-card.visible { + opacity: 1; + transform: translateY(0); +} + +.essentials-card-interactive { + min-height: 600px; +} + +/* Three distinct sections - no cards */ +.essentials-section-item { + margin-bottom: 80px; + opacity: 1; + transform: translateY(0); +} + +.essentials-content-wrapper { + margin-bottom: 40px; + max-width: 800px; +} + +.essentials-content-wrapper h3 { + display: flex; + align-items: center; + gap: 12px; + font-size: 2rem; + font-weight: 700; + color: var(--lp-c-text-1); + margin-bottom: 16px; +} + +.essentials-content-wrapper h3 svg { + color: var(--lp-c-brand-2); + flex-shrink: 0; +} + +.essentials-content-wrapper p { + font-size: 1.1rem; + line-height: 1.7; + color: var(--lp-c-text-2); + margin: 0; +} + +.essentials-visual-standalone { + position: relative; + min-height: 550px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(4, 159, 154, 0.02); + border-radius: 16px; + padding: 60px 40px; +} + +[data-theme='dark'] .essentials-visual-standalone { + background: rgba(44, 224, 191, 0.03); +} + +.standalone-cp { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 280px; + filter: drop-shadow(0 6px 24px rgba(4, 159, 154, 0.4)); + z-index: 10; + pointer-events: none; +} + +/* Unified scrollable section */ +.essentials-unified { + display: grid; + grid-template-columns: 350px 1fr; + gap: 40px; + min-height: 600px; +} + +.essentials-sidebar { + display: flex; + flex-direction: column; + gap: 20px; +} + +.essentials-buttons { + display: flex; + flex-direction: column; + gap: 12px; +} + +.essentials-nav-button { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: rgba(4, 159, 154, 0.05); + border: 2px solid transparent; + border-radius: 12px; + color: var(--lp-c-text-2); + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +[data-theme='dark'] .essentials-nav-button { + background: rgba(44, 224, 191, 0.08); +} + +.essentials-nav-button:hover { + background: rgba(4, 159, 154, 0.12); + color: var(--lp-c-text-1); + transform: translateX(4px); +} + +.essentials-nav-button.active { + background: rgba(4, 159, 154, 0.15); + border-color: var(--lp-c-brand-2); + color: var(--lp-c-brand-2); +} + +[data-theme='dark'] .essentials-nav-button.active { + background: rgba(44, 224, 191, 0.2); + border-color: var(--lp-c-brand-2); +} + +.essentials-nav-button svg { + flex-shrink: 0; +} + +.essentials-scroll-container { + overflow-y: auto; + height: 500px; + padding-right: 10px; + scroll-behavior: smooth; +} + +.essentials-scroll-container::-webkit-scrollbar { + width: 6px; +} + +.essentials-scroll-container::-webkit-scrollbar-track { + background: rgba(4, 159, 154, 0.05); + border-radius: 10px; +} + +.essentials-scroll-container::-webkit-scrollbar-thumb { + background: rgba(4, 159, 154, 0.3); + border-radius: 10px; +} + +.essentials-scroll-container::-webkit-scrollbar-thumb:hover { + background: rgba(4, 159, 154, 0.5); +} + +.essentials-scroll-item { + min-height: 450px; + padding: 30px 20px; + margin-bottom: 30px; +} + +.essentials-scroll-item h3 { + font-size: 1.5rem; + font-weight: 600; + color: var(--lp-c-text-1); + margin-bottom: 12px; +} + +.essentials-scroll-item p { + font-size: 1rem; + line-height: 1.7; + color: var(--lp-c-text-2); +} + +/* Content flip area - text changes based on active feature */ +.essentials-content-flip { + position: relative; + min-height: 200px; + margin-top: 20px; +} + +.essentials-content-item { + position: absolute; + top: 0; + left: 0; + right: 0; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.3s ease, transform 0.3s ease; + pointer-events: none; +} + +.essentials-content-item.active { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.essentials-content-item h3 { + font-size: 1.8rem; + font-weight: 600; + color: var(--lp-c-text-1); + margin-bottom: 16px; +} + +.essentials-content-item p { + font-size: 1.05rem; + line-height: 1.7; + color: var(--lp-c-text-2); + margin: 0; +} + +.essentials-visual-unified { + position: relative; + min-height: 480px; + border-radius: 16px; + padding: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +.unified-cp { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 280px; + filter: drop-shadow(0 6px 24px rgba(4, 159, 154, 0.4)); + z-index: 10; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; +} + +.unified-cp.active { + opacity: 1; +} + +/* YAML view - simplified with code on left, cloud on right */ +.yaml-left-container { + position: absolute; + left: 40px; + top: 50%; + transform: translateY(-50%); + max-width: 300px; +} + +.essentials-yaml-unified { + background: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(44, 224, 191, 0.3); + border-radius: 8px; + padding: 16px; + font-size: 0.75rem; + line-height: 1.4; + color: #e0e0e0; + font-family: 'Courier New', monospace; + margin: 0; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +.yaml-right-container { + position: absolute; + right: -80px; + top: 50%; + transform: translateY(-50%); +} + +.yaml-cloud-right { + opacity: 0; + transform: translateX(-80px) scale(0.8); + transition: all 0.6s ease-out; +} + +.yaml-cloud-right.animate { + opacity: 1; + transform: translateX(0) scale(1); +} + +/* Icon turning green animations */ +.yaml-cloud-right.animate .yaml-cloud-icon-database ellipse, +.yaml-cloud-right.animate .yaml-cloud-icon-database path { + animation: icon-turn-green 0.6s ease-out 1.5s forwards; +} + +.yaml-cloud-right.animate .yaml-cloud-icon-account path, +.yaml-cloud-right.animate .yaml-cloud-icon-account circle { + animation: icon-turn-green 0.6s ease-out 2.3s forwards; +} + +.yaml-cloud-right.animate .yaml-cloud-shape { + animation: cloud-turn-green 0.8s ease-out 3.1s forwards; +} + +@keyframes icon-turn-green { + 0% { + stroke: rgba(147, 51, 234, 0.9); + } + 100% { + stroke: rgba(34, 197, 94, 0.9); + } +} + +@keyframes cloud-turn-green { + 0% { + stroke: rgba(147, 51, 234, 0.5); + fill: url(#yamlCloudGradient); + } + 100% { + stroke: rgba(34, 197, 94, 0.6); + fill: rgba(34, 197, 94, 0.15); + } +} + +/* Animated line connection - same style as hero */ +.yaml-connection-line { + opacity: 0; +} + +.yaml-connection-line.animate { + opacity: 1; +} + +.yaml-connection-line line { + animation: connection-pulse 3s ease-in-out infinite; + stroke-width: 2; +} + +[data-theme='dark'] .yaml-connection-line line { + stroke: rgba(44, 224, 191, 0.3); +} + +/* Large interactive card with tabs */ +.essentials-card-large { + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(20px); + border-radius: 24px; + border: 2px solid rgba(4, 159, 154, 0.15); + box-shadow: 0 8px 32px rgba(44, 224, 191, 0.08); + margin-bottom: 30px; + overflow: hidden; + opacity: 1; + transform: translateY(0); + transition: all 0.3s ease-out; +} + +[data-theme='dark'] .essentials-card-large { + background: rgba(32, 33, 39, 0.7); + border: 1px solid rgba(44, 224, 191, 0.2); + box-shadow: 0 8px 32px rgba(44, 224, 191, 0.12); +} + +.essentials-interactive-new { + display: flex; + flex-direction: column; +} + +/* Horizontal tabs at top */ +.essentials-tabs { + display: flex; + gap: 0; + border-bottom: 2px solid rgba(4, 159, 154, 0.2); + background: rgba(4, 159, 154, 0.03); +} + +.essentials-tab { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 20px 24px; + border: none; + background: transparent; + color: var(--lp-c-text-2); + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.2s ease; + position: relative; +} + +.essentials-tab:hover { + background: rgba(4, 159, 154, 0.08); + color: var(--lp-c-brand-2); +} + +.essentials-tab.active { + color: var(--lp-c-brand-2); + background: rgba(4, 159, 154, 0.1); +} + +.essentials-tab.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: var(--lp-c-brand-2); +} + +.essentials-tab svg { + flex-shrink: 0; +} + +/* Large visual area */ +.essentials-visual-large { + position: relative; + min-height: 550px; + padding: 60px 40px; + background: rgba(4, 159, 154, 0.02); +} + +/* Descriptions below animations */ +.essentials-descriptions { + position: relative; + padding: 40px 50px; + border-top: 1px solid rgba(4, 159, 154, 0.15); + background: rgba(255, 255, 255, 0.3); + min-height: 140px; +} + +[data-theme='dark'] .essentials-descriptions { + background: rgba(0, 0, 0, 0.2); +} + +.essentials-description { + display: none; +} + +.essentials-description.active { + display: block; + animation: description-fade-in 0.3s ease-out; +} + +.essentials-description h3 { + font-size: 1.5rem; + margin-bottom: 12px; + color: var(--lp-c-text-1); +} + +.essentials-description p { + font-size: 1rem; + line-height: 1.6; + color: var(--lp-c-text-2); + margin: 0; +} + +@keyframes description-fade-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Compact feature cards */ +.essentials-features-compact { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-top: 0; +} + +.essentials-feature-card { + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(20px); + border-radius: 16px; + border: 2px solid rgba(4, 159, 154, 0.15); + padding: 30px; + opacity: 1; + transform: translateY(0); + transition: all 0.3s ease-out; +} + +[data-theme='dark'] .essentials-feature-card { + background: rgba(32, 33, 39, 0.7); + border: 1px solid rgba(44, 224, 191, 0.2); + box-shadow: 0 4px 16px rgba(44, 224, 191, 0.08); +} + +.essentials-feature-card h3 { + font-size: 1.2rem; + margin-bottom: 20px; + color: var(--lp-c-text-1); +} + +.essentials-feature-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.essentials-feature-item-compact { + display: flex; + align-items: center; + gap: 10px; + padding: 12px; + background: rgba(4, 159, 154, 0.05); + border-radius: 8px; + color: var(--lp-c-text-2); + font-size: 0.9rem; + transition: all 0.2s ease; +} + +[data-theme='dark'] .essentials-feature-item-compact { + background: rgba(44, 224, 191, 0.08); +} + +.essentials-feature-item-compact:hover { + background: rgba(4, 159, 154, 0.12); + transform: translateX(2px); +} + +.essentials-feature-item-compact svg { + flex-shrink: 0; + color: var(--lp-c-brand-2); +} + +/* Elegant feature lists */ +.essentials-features-elegant { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 60px; + margin-top: 50px; + padding: 0 20px; +} + +.essentials-feature-section h4 { + font-size: 1.1rem; + font-weight: 600; + color: var(--lp-c-text-1); + margin-bottom: 20px; + opacity: 0.9; +} + +.essentials-feature-list-elegant { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.essentials-feature-list-elegant li { + display: flex; + align-items: center; + gap: 12px; + color: var(--lp-c-text-2); + font-size: 0.95rem; + padding: 8px 0; + border-bottom: 1px solid rgba(4, 159, 154, 0.08); + transition: all 0.2s ease; +} + +[data-theme='dark'] .essentials-feature-list-elegant li { + border-bottom: 1px solid rgba(44, 224, 191, 0.12); +} + +.essentials-feature-list-elegant li:last-child { + border-bottom: none; +} + +.essentials-feature-list-elegant li:hover { + color: var(--lp-c-text-1); + padding-left: 4px; +} + +.essentials-feature-list-elegant li svg { + flex-shrink: 0; + color: var(--lp-c-brand-2); + opacity: 0.7; +} + +.essentials-feature-list-elegant li:hover svg { + opacity: 1; +} + +[data-theme='dark'] .essentials-card { + background: rgba(32, 33, 39, 0.7); + border: 1px solid rgba(44, 224, 191, 0.2); + box-shadow: 0 8px 32px rgba(44, 224, 191, 0.12); +} + +.essentials-card::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 24px; + padding: 2px; + background: linear-gradient(135deg, rgba(4, 159, 154, 0.3), rgba(44, 224, 191, 0.1)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 0.4s ease; +} + +.essentials-card:hover::before { + opacity: 1; +} + +.essentials-card-number { + font-size: 5rem; + font-weight: 900; + line-height: 1; + color: var(--teal-6); + opacity: 0.15; + flex-shrink: 0; + font-family: ui-monospace, monospace; +} + +[data-theme='dark'] .essentials-card-number { + color: var(--teal-4); + opacity: 0.2; +} + +.essentials-card-content { + flex: 1; +} + +.essentials-card-content h3 { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 24px; + color: var(--teal-10); + line-height: 1.3; +} + +[data-theme='dark'] .essentials-card-content h3 { + color: var(--teal-2); +} + +/* Interactive section 1 layout */ +.essentials-interactive { + display: grid; + grid-template-columns: 300px 1fr; + gap: 50px; + align-items: center; +} + +.essentials-feature-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.essentials-feature-item { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: rgba(4, 159, 154, 0.05); + border-radius: 12px; + border: 2px solid rgba(4, 159, 154, 0.1); + transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + font-size: 1.05rem; + font-weight: 500; + color: var(--teal-10); + cursor: pointer; + position: relative; + width: 100%; + text-align: left; + font-family: inherit; +} + +.essentials-feature-item:hover { + background: rgba(4, 159, 154, 0.1); + border-color: rgba(4, 159, 154, 0.3); + transform: translateX(4px); +} + +[data-theme='dark'] .essentials-feature-item { + background: rgba(44, 224, 191, 0.08); + border: 2px solid rgba(44, 224, 191, 0.15); + color: var(--teal-2); +} + +[data-theme='dark'] .essentials-feature-item:hover { + background: rgba(44, 224, 191, 0.12); + border-color: rgba(44, 224, 191, 0.3); +} + +.essentials-feature-item svg { + flex-shrink: 0; + color: var(--teal-6); + transition: transform 0.3s ease; +} + +[data-theme='dark'] .essentials-feature-item svg { + color: var(--teal-4); +} + +.essentials-feature-item.active { + background: rgba(4, 159, 154, 0.15); + border-color: rgba(4, 159, 154, 0.5); + transform: translateX(8px) scale(1.03); + box-shadow: 0 4px 20px rgba(4, 159, 154, 0.2); +} + +[data-theme='dark'] .essentials-feature-item.active { + background: rgba(44, 224, 191, 0.2); + border-color: rgba(44, 224, 191, 0.6); + box-shadow: 0 4px 20px rgba(44, 224, 191, 0.25); +} + +.essentials-feature-item.active svg { + transform: scale(1.15); + color: var(--teal-4); +} + +[data-theme='dark'] .essentials-feature-item.active svg { + color: var(--teal-2); +} + +.essentials-visual { + position: relative; + min-height: 600px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(4, 159, 154, 0.02); + border-radius: 16px; + padding: 40px; +} + +[data-theme='dark'] .essentials-visual { + background: rgba(44, 224, 191, 0.03); +} + +.essentials-visual-content { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: scale(0.9); + transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + pointer-events: none; +} + +.essentials-visual-content.active { + opacity: 1; + transform: scale(1); + pointer-events: auto; +} + +/* Connection line animations */ +.morphing-cp-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10; + pointer-events: none; +} + +.morphing-cp { + display: block; + width: 120px; + filter: drop-shadow(0 6px 24px rgba(4, 159, 154, 0.4)); + transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +/* YAML view layout */ +.yaml-top-container { + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: 400px; + opacity: 0; +} + +.yaml-top-container.animate { + animation: yaml-fade-in 0.3s ease-out forwards; +} + +.yaml-bottom-container { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: 400px; + opacity: 0; +} + +.yaml-bottom-container.animate { + animation: yaml-fade-in 0.3s ease-out 0.5s forwards; +} + +@keyframes yaml-fade-in { + to { + opacity: 1; + } +} + +.morphing-cp { + display: block; + width: 280px; + filter: drop-shadow(0 6px 24px rgba(4, 159, 154, 0.4)); + transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.8s ease, transform 0.8s ease; + opacity: 0; + transform: translateX(100px); +} + +/* Show CP when any feature is active */ +.essentials-visual-large:has(.essentials-visual-content.active) .morphing-cp { + opacity: 1; + transform: translateX(0); +} + +/* Slide-in animation only for feature 0 (YAML) */ +.essentials-visual-large:has(.essentials-visual-content:nth-child(2).active) .morphing-cp { + animation: cp-slide-in 0.3s ease-out 0.3s forwards; +} + +@keyframes cp-slide-in { + from { + opacity: 0; + transform: translateX(100px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.essentials-yaml-compact { + background: rgba(4, 159, 154, 0.05); + border: 1px solid rgba(4, 159, 154, 0.2); + border-radius: 8px; + padding: 16px; + font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace; + font-size: 0.75rem; + line-height: 1.5; + color: var(--teal-9); + margin: 0; + white-space: pre; + overflow: visible; +} + +[data-theme='dark'] .essentials-yaml-compact { + background: rgba(4, 159, 154, 0.08); + border-color: rgba(4, 159, 154, 0.3); + color: var(--teal-3); +} + +.yaml-animation-area { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + position: absolute; + top: 50%; + left: 30%; + transform: translate(-50%, -50%); +} + +.yaml-cloud { + display: block; + opacity: 0; +} + +.yaml-cloud.animate { + animation: yaml-cloud-fade-in 0.3s ease-out 0.8s forwards; +} + +@keyframes yaml-cloud-fade-in { + to { + opacity: 1; + } +} + +.yaml-to-cp-line { + display: block; + opacity: 0; +} + +.yaml-to-cp-line.animate { + animation: yaml-cloud-fade-in 0.3s ease-out 0.8s forwards; +} + +/* GitOps view layout */ +.gitops-animation-area { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.gitops-cloud-original { + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% - 280px), -50%); + opacity: 1; +} + +.gitops-cloud-original.animate { + animation: gitops-slide-left 0.8s ease-in-out forwards; +} + +.gitops-cloud-replica { + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% + 280px), -50%); + opacity: 0; +} + +.gitops-cloud-replica.animate { + animation: gitops-fade-slide-right 0.8s ease-in-out 0.3s forwards; +} + +.gitops-conn-left, +.gitops-conn-right { + stroke-width: 1; + stroke-dasharray: 3, 3; + opacity: 0; +} + +.gitops-conn-left.animate, +.gitops-conn-right.animate { + opacity: 1; + animation: connection-pulse 3s ease-in-out infinite; +} + +[data-theme='dark'] .gitops-conn-left, +[data-theme='dark'] .gitops-conn-right { + stroke: rgba(44, 224, 191, 0.3); +} + +@keyframes gitops-line-draw { + to { + opacity: 1; + } +} + +@keyframes gitops-slide-left { + to { + transform: translateX(0); + } +} + +@keyframes gitops-fade-slide-right { + to { + opacity: 1; + transform: translateX(0); + } +} + +.gitops-cloud-original.animate { + animation: gitops-slide-left 1s ease-in-out forwards; +} + +.gitops-cloud-replica { + opacity: 0; +} + +.gitops-cloud-replica.animate { + animation: gitops-fade-slide-right 1s ease-in-out 0.3s forwards; +} + +.gitops-cloud-original { + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% - 280px), -50%); + opacity: 1; +} + +.gitops-cloud-original.animate { + animation: gitops-slide-left 0.8s ease-in-out forwards; +} + +.gitops-cloud-replica { + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% + 280px), -50%); + opacity: 0; +} + +.gitops-cloud-replica.animate { + animation: gitops-fade-slide-right 0.8s ease-in-out 0.3s forwards; +} + +@keyframes gitops-slide-left { + 0% { + transform: translate(calc(-50% - 280px), -50%); + } + 100% { + transform: translate(calc(-50% - 280px), -50%); + } +} + +@keyframes gitops-fade-slide-right { + 0% { + opacity: 0; + transform: translate(calc(-50% + 180px), -50%); + } + 100% { + opacity: 1; + transform: translate(calc(-50% + 280px), -50%); + } +} + +.gitops-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.gitops-conn-left, +.gitops-conn-right { + stroke: rgba(147, 51, 234, 0.5); + stroke-width: 2.5; + stroke-dasharray: 6, 4; + opacity: 0; + transition: opacity 0.5s ease; +} + +.essentials-visual-content.active .gitops-conn-left, +.essentials-visual-content.active .gitops-conn-right { + opacity: 0.8; +} + +.gitops-description { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + max-width: 500px; + text-align: center; + color: var(--teal-10); + font-size: 0.9rem; + line-height: 1.6; +} + +[data-theme='dark'] .gitops-description { + color: var(--teal-3); +} + +/* Self-healing view - many clouds popping up */ +.selfhealing-animation-area { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +/* Self-healing resource icons */ +.healing-resource { + position: absolute; + display: flex; + align-items: center; + opacity: 0; +} + +.healing-resource.animate { + animation: resource-popup 1.5s ease-out forwards; +} + +@keyframes resource-popup { + 0% { + opacity: 0; + transform: scale(0.5); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.healing-resource-icon { + width: 80px; + height: 80px; + background: rgba(4, 159, 154, 0.1); + border: 2px solid rgba(44, 224, 191, 0.4); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 5; +} + +[data-theme='dark'] .healing-resource-icon { + background: rgba(44, 224, 191, 0.15); + border-color: rgba(44, 224, 191, 0.5); +} + +.resource-icon-stroke { + stroke: var(--lp-c-brand-2); +} + +/* Position resources around CP */ +.healing-resource-1 { + top: 5%; + left: 50%; + transform: translateX(-50%); + flex-direction: column; + animation-delay: 0s; +} + +.healing-resource-2 { + top: 50%; + right: 8%; + transform: translateY(-50%); + flex-direction: row-reverse; + animation-delay: 0.3s; +} + +.healing-resource-3 { + bottom: 5%; + left: 50%; + transform: translateX(-50%); + flex-direction: column-reverse; + animation-delay: 0.6s; +} + +.healing-resource-4 { + top: 50%; + left: 8%; + transform: translateY(-50%); + flex-direction: row; + animation-delay: 0.9s; +} + +/* Connection lines */ +.healing-conn-line-simple { + display: block; +} + +.healing-conn-simple { + stroke: rgba(147, 51, 234, 0.2); + stroke-width: 2; + stroke-dasharray: 3, 3; + stroke-dashoffset: 0; +} + +.healing-resource.animate .healing-conn-simple { + animation: connection-pulse 3s ease-in-out 1.5s infinite; +} + +[data-theme='dark'] .healing-conn-simple { + stroke: rgba(44, 224, 191, 0.3); +} + +@keyframes line-draw { + to { + stroke-dashoffset: 0; + } +} + +@keyframes line-pulse { + 0%, 100% { + opacity: 0.5; + } + 50% { + opacity: 1; + stroke: rgba(44, 224, 191, 0.8); + } +} + +/* Resource 3 fails and recovers */ +.healing-resource-3.animate .resource-icon-stroke { + animation: resource-failure 13s ease-in-out 3s infinite; +} + +.healing-resource-3.animate .healing-conn-simple { + animation: line-draw 1s ease-out 0.5s forwards, line-failure 13s ease-in-out 3s infinite; +} + +@keyframes resource-failure { + 0%, 25% { + stroke: var(--lp-c-brand-2); + } + 30%, 50% { + stroke: #ef4444; + } + 55%, 65% { + stroke: #22c55e; + } + 70%, 100% { + stroke: var(--lp-c-brand-2); + } +} + +@keyframes line-failure { + 0%, 25% { + stroke: rgba(44, 224, 191, 0.5); + } + 30%, 50% { + stroke: rgba(239, 68, 68, 0.7); + } + 55%, 65% { + stroke: rgba(34, 197, 94, 0.7); + } + 70%, 100% { + stroke: rgba(44, 224, 191, 0.5); + } +} + +.healing-cloud { + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + opacity: 0; +} + +.healing-cloud.animate { + animation: healing-cloud-popup 2s ease-in-out infinite; +} + +.healing-cloud-1 { + top: 5%; + left: 50%; + transform: translateX(-50%); + animation-delay: 0s; +} + +.healing-cloud-1 .healing-conn-line { + transform: rotate(180deg); +} + +.healing-cloud-2 { + top: 22%; + right: 12%; + animation-delay: 2s; +} + +.healing-cloud-2 .healing-conn-line { + transform: rotate(225deg); +} + +.healing-cloud-3 { + bottom: 22%; + right: 12%; + animation-delay: 4s; +} + +.healing-cloud-3 .healing-conn-line { + transform: rotate(315deg); +} + +.healing-cloud-4 { + bottom: 5%; + left: 50%; + transform: translateX(-50%); + animation-delay: 6s; +} + +.healing-cloud-4 .healing-conn-line { + transform: rotate(0deg); +} + +.healing-cloud-5 { + bottom: 22%; + left: 12%; + animation-delay: 8s; +} + +.healing-cloud-5 .healing-conn-line { + transform: rotate(45deg); +} + +.healing-cloud-6 { + top: 22%; + left: 12%; + animation-delay: 10s; +} + +.healing-cloud-6 .healing-conn-line { + transform: rotate(45deg); +} + +/* Cloud 3 fails and recovers every 13 seconds */ +.healing-cloud-3.animate { + animation: healing-cloud-failure 13s ease-in-out 4s infinite; +} + +@keyframes healing-cloud-popup { + 0%, 100% { + opacity: 0; + transform: scale(0.8); + } + 10%, 90% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes healing-cloud-failure { + 0%, 100% { + opacity: 0; + transform: scale(0.8); + } + 7.7%, 92.3% { + opacity: 1; + transform: scale(1); + } +} + +.healing-cloud-hex { + stroke: rgba(44, 224, 191, 0.5); + transition: stroke 0.6s ease; +} + +.healing-cloud-db { + stroke: rgba(44, 224, 191, 0.8); + transition: stroke 0.6s ease; +} + +/* Different colors for each cloud */ +.healing-cloud-1 .cloud-gradient-start { stop-color: rgba(123, 97, 255, 0.12); } +.healing-cloud-1 .cloud-gradient-end { stop-color: rgba(147, 51, 234, 0.08); } +.healing-cloud-1 .healing-cloud-hex { stroke: rgba(147, 51, 234, 0.5); } +.healing-cloud-1 .healing-cloud-db { stroke: rgba(147, 51, 234, 0.8); } + +.healing-cloud-2 .cloud-gradient-start { stop-color: rgba(4, 159, 154, 0.12); } +.healing-cloud-2 .cloud-gradient-end { stop-color: rgba(44, 224, 191, 0.08); } +.healing-cloud-2 .healing-cloud-hex { stroke: rgba(4, 159, 154, 0.5); } +.healing-cloud-2 .healing-cloud-db { stroke: rgba(4, 159, 154, 0.8); } + +.healing-cloud-3 .cloud-gradient-start { stop-color: rgba(59, 130, 246, 0.12); } +.healing-cloud-3 .cloud-gradient-end { stop-color: rgba(96, 165, 250, 0.08); } +.healing-cloud-3 .healing-cloud-hex { stroke: rgba(59, 130, 246, 0.5); } +.healing-cloud-3 .healing-cloud-db { stroke: rgba(59, 130, 246, 0.8); } + +.healing-cloud-4 .cloud-gradient-start { stop-color: rgba(34, 197, 94, 0.12); } +.healing-cloud-4 .cloud-gradient-end { stop-color: rgba(74, 222, 128, 0.08); } +.healing-cloud-4 .healing-cloud-hex { stroke: rgba(34, 197, 94, 0.5); } +.healing-cloud-4 .healing-cloud-db { stroke: rgba(34, 197, 94, 0.8); } + +.healing-cloud-5 .cloud-gradient-start { stop-color: rgba(236, 72, 153, 0.12); } +.healing-cloud-5 .cloud-gradient-end { stop-color: rgba(244, 114, 182, 0.08); } +.healing-cloud-5 .healing-cloud-hex { stroke: rgba(236, 72, 153, 0.5); } +.healing-cloud-5 .healing-cloud-db { stroke: rgba(236, 72, 153, 0.8); } + +.healing-cloud-6 .cloud-gradient-start { stop-color: rgba(251, 146, 60, 0.12); } +.healing-cloud-6 .cloud-gradient-end { stop-color: rgba(251, 191, 36, 0.08); } +.healing-cloud-6 .healing-cloud-hex { stroke: rgba(251, 146, 60, 0.5); } +.healing-cloud-6 .healing-cloud-db { stroke: rgba(251, 146, 60, 0.8); } + +/* Cloud 3 failure colors */ +.healing-cloud-3.animate .healing-cloud-db { + animation: healing-db-failure-recovery 13s ease-in-out 4s infinite; +} + +.healing-cloud-3.animate .healing-cloud-hex { + animation: healing-hex-failure-recovery 13s ease-in-out 4s infinite; +} + +@keyframes healing-db-failure-recovery { + 0%, 100% { + stroke: rgba(59, 130, 246, 0.8); + } + 30%, 50% { + stroke: rgba(239, 68, 68, 0.9); + } + 60%, 70% { + stroke: rgba(34, 197, 94, 0.9); + } + 80% { + stroke: rgba(59, 130, 246, 0.8); + } +} + +@keyframes healing-hex-failure-recovery { + 0%, 100% { + stroke: rgba(59, 130, 246, 0.5); + } + 30%, 50% { + stroke: rgba(239, 68, 68, 0.7); + } + 60%, 70% { + stroke: rgba(34, 197, 94, 0.7); + } + 80% { + stroke: rgba(59, 130, 246, 0.5); + } +} + +.healing-conn-line { + position: absolute; + left: 50%; + top: 50%; + transform-origin: 30px 15px; +} + +.healing-conn { + stroke: rgba(44, 224, 191, 0.5); + stroke-dasharray: 6, 4; + opacity: 0; +} + +.healing-cloud.animate .healing-conn { + animation: healing-conn-pulse 4s ease-in-out infinite; +} + +.healing-cloud-3.animate .healing-conn { + animation: healing-conn-failure-recovery 13s ease-in-out 4s infinite; +} + +@keyframes healing-conn-pulse { + 0%, 100% { + opacity: 0; + } + 25%, 75% { + opacity: 0.8; + } +} + +@keyframes healing-conn-failure-recovery { + 0%, 100% { + opacity: 0; + } + 25% { + opacity: 0.8; + stroke: rgba(44, 224, 191, 0.5); + stroke-width: 2.5; + } + 30%, 50% { + opacity: 1; + stroke: rgba(239, 68, 68, 0.95); + stroke-width: 4; + } + 60%, 70% { + opacity: 1; + stroke: rgba(34, 197, 94, 0.8); + stroke-width: 3; + } + 75% { + opacity: 0.8; + stroke: rgba(44, 224, 191, 0.5); + stroke-width: 2.5; + } +} + +.yaml-connection-line line { + animation: connection-pulse 3s ease-in-out infinite; +} + +@keyframes connection-pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +.db-connection { + animation: db-connection-pulse 3s ease-in-out infinite; +} + +@keyframes db-connection-pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 0.9; } +} + +.gitops-connection { + animation: connection-pulse 3s ease-in-out infinite; +} + +/* Declarative API visualization */ +.yaml-demo-layout { + display: flex; + flex-direction: column; + align-items: center; + gap: 30px; + width: 100%; +} + +.yaml-cp-cloud { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.demo-cloud-top { + filter: drop-shadow(0 4px 16px rgba(147, 51, 234, 0.25)); +} + +.essentials-yaml { + font-size: 0.85rem; +} + +/* Icons inside control plane */ +.cp-resource-icon { + opacity: 0; + transform: scale(0.5); +} + +.essentials-visual-content.active .cp-resource-icon { + animation: icon-pop-in 0.6s ease-out forwards; +} + +.icon-user-cp { + animation-delay: 0.8s; +} + +.icon-database-cp { + animation-delay: 1.1s; +} + +@keyframes icon-pop-in { + 0% { + opacity: 0; + transform: scale(0.3); + } + 50% { + transform: scale(1.15); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes demo-cp-float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +.demo-cloud { + animation: demo-cloud-appear 0.8s ease-out 0.3s backwards; +} + +.essentials-cp-cloud-demo { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 220px; +} + +@keyframes demo-cloud-appear { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* YAML container and typing animation */ +.essentials-yaml-container { + position: relative; + width: 100%; +} + +.essentials-yaml { + background: rgba(4, 159, 154, 0.05); + border: 1px solid rgba(4, 159, 154, 0.2); + border-radius: 12px; + padding: 24px; + font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace; + font-size: 0.85rem; + line-height: 1.6; + color: var(--teal-10); + margin: 0; + box-shadow: 0 8px 32px rgba(4, 159, 154, 0.15); + position: relative; + overflow: hidden; + max-width: 480px; +} + +.essentials-yaml.typing { + animation: yaml-glow 2s ease-in-out 3s infinite; +} + +.essentials-yaml.typing::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, + rgba(4, 159, 154, 0) 0%, + rgba(44, 224, 191, 0.4) 50%, + rgba(4, 159, 154, 0) 100%); + animation: yaml-typing-wave 3s ease-in-out forwards; + pointer-events: none; + filter: blur(12px); +} + +@keyframes yaml-typing-wave { + 0% { + transform: translateX(-100%); + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateX(100%); + opacity: 0; + } +} + +[data-theme='dark'] .essentials-yaml { + background: rgba(44, 224, 191, 0.08); + border-color: rgba(44, 224, 191, 0.3); + color: var(--teal-2); +} + +@keyframes yaml-glow { + 0%, 100% { + box-shadow: 0 4px 16px rgba(4, 159, 154, 0.1); + } + 50% { + box-shadow: 0 4px 24px rgba(4, 159, 154, 0.25); + } +} + +/* Self-healing visualization */ +.essentials-selfhealing-container { + position: relative; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 40px; +} + +.selfhealing-cp { + width: 120px; + height: auto; + filter: drop-shadow(0 6px 24px rgba(4, 159, 154, 0.4)); + animation: demo-cp-float 4s ease-in-out infinite; +} + +.database-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + width: 100%; + max-width: 200px; + position: relative; +} + +/* Centered cloud circle layout for self-healing */ +.cloud-circle-layout { + position: relative; + width: 400px; + height: 400px; + margin: 0 auto; +} + +.cloud-item-centered { + position: absolute; + display: flex; + align-items: center; + justify-content: center; +} + +.cloud-item-centered.cloud-top { + top: 0; + left: 50%; + transform: translateX(-50%); +} + +.cloud-item-centered.cloud-right { + top: 50%; + right: 0; + transform: translateY(-50%); +} + +.cloud-item-centered.cloud-bottom { + bottom: 0; + left: 50%; + transform: translateX(-50%); +} + +.cloud-item-centered.cloud-left { + top: 50%; + left: 0; + transform: translateY(-50%); +} + +.cloud-item-centered .cloud-db-stroke { + stroke: rgba(44, 224, 191, 0.8); + fill: none; + transition: stroke 0.8s ease; +} + +.cloud-item-centered .cloud-hex { + transition: stroke 0.8s ease; +} + +/* Cloud 3 (bottom) failure animation */ +.cloud-item-centered.cloud-3.animate .cloud-db-stroke { + animation: cloud-db-failure-recovery 20s ease-in-out 5s infinite; +} + +.cloud-item-centered.cloud-3.animate .cloud-hex { + animation: cloud-hex-failure-recovery 20s ease-in-out 5s infinite; +} + +@keyframes cloud-db-failure-recovery { + 0%, 100% { + stroke: rgba(44, 224, 191, 0.8); + } + 5%, 15% { + stroke: rgba(239, 68, 68, 0.9); + } + 20%, 30% { + stroke: rgba(34, 197, 94, 0.9); + } + 35% { + stroke: rgba(44, 224, 191, 0.8); + } +} + +@keyframes cloud-hex-failure-recovery { + 0%, 100% { + stroke: rgba(59, 130, 246, 0.5); + } + 5%, 15% { + stroke: rgba(239, 68, 68, 0.7); + } + 20%, 30% { + stroke: rgba(34, 197, 94, 0.7); + } + 35% { + stroke: rgba(59, 130, 246, 0.5); + } +} + +.cloud-connections-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +.cloud-conn { + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + stroke-dasharray: 6, 4; + opacity: 0; + transition: opacity 0.3s ease, stroke 0.8s ease; +} + +.essentials-visual-content.active .cloud-conn { + opacity: 1; +} + +.cloud-connections-overlay .cloud-conn { + animation: cloud-conn-pulse-stagger 4s ease-in-out infinite; +} + +.cloud-connections-overlay .cloud-conn-1 { + animation-delay: 0s; +} + +.cloud-connections-overlay .cloud-conn-2 { + animation-delay: 1s; +} + +.cloud-connections-overlay .cloud-conn-3 { + animation-delay: 2s; +} + +.cloud-connections-overlay .cloud-conn-4 { + animation-delay: 3s; +} + +.cloud-item-centered.cloud-3.animate ~ .cloud-connections-overlay .cloud-conn-3 { + animation: cloud-conn-failure 20s ease-in-out 5s infinite; +} + +@keyframes cloud-conn-pulse-stagger { + 0%, 15%, 100% { + opacity: 0; + } + 5%, 10% { + opacity: 0.9; + } +} + +@keyframes cloud-conn-failure { + 0%, 100% { + opacity: 0; + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + } + 5%, 15% { + opacity: 1; + stroke: rgba(239, 68, 68, 0.95); + stroke-width: 4; + } + 20%, 30% { + opacity: 1; + stroke: rgba(34, 197, 94, 0.8); + stroke-width: 3; + } + 35% { + opacity: 0; + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + } +} + +/* Centered database circle layout */ +.database-circle-layout { + position: relative; + width: 400px; + height: 400px; + margin: 0 auto; +} + +.database-item-centered { + position: absolute; + display: flex; + align-items: center; + justify-content: center; +} + +.database-item-centered.db-top { + top: 0; + left: 50%; + transform: translateX(-50%); +} + +.database-item-centered.db-right { + top: 50%; + right: 0; + transform: translateY(-50%); +} + +.database-item-centered.db-bottom { + bottom: 0; + left: 50%; + transform: translateX(-50%); +} + +.database-item-centered.db-left { + top: 50%; + left: 0; + transform: translateY(-50%); +} + +.database-item-centered .db-stroke { + stroke: rgba(44, 224, 191, 0.8); + fill: none; + transition: stroke 0.5s ease; +} + +.database-item-centered.db-3.animate { + animation: db-failure-recovery 8s ease-in-out 3s infinite; +} + +.database-item-centered.db-3.animate .db-stroke { + animation: db-stroke-failure-recovery 8s ease-in-out 3s infinite; +} + +.db-connections-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +.db-conn { + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + stroke-dasharray: 6, 4; + opacity: 0; + transition: opacity 0.3s ease, stroke 0.5s ease; +} + +.database-item-centered.animate ~ .db-connections-overlay .db-conn { + opacity: 1; +} + +.db-connections-overlay .db-conn { + animation: db-conn-pulse-stagger 4s ease-in-out infinite; +} + +.db-connections-overlay .db-conn-1 { + animation-delay: 0s; +} + +.db-connections-overlay .db-conn-2 { + animation-delay: 1s; +} + +.db-connections-overlay .db-conn-3 { + animation-delay: 2s; +} + +.db-connections-overlay .db-conn-4 { + animation-delay: 3s; +} + +.database-item-centered.db-3.animate ~ .db-connections-overlay .db-conn-3 { + animation: db-conn-failure 8s ease-in-out 3s infinite; +} + +@keyframes db-conn-pulse-stagger { + 0%, 15%, 100% { + opacity: 0; + } + 5%, 10% { + opacity: 0.9; + } +} + +@keyframes db-conn-failure { + 0%, 100% { + opacity: 0; + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + } + 12.5%, 37.5% { + opacity: 1; + stroke: rgba(239, 68, 68, 0.95); + stroke-width: 4; + } +} + +.db-connections-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +.db-conn { + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + stroke-dasharray: 6, 4; + opacity: 0; + transition: opacity 0.3s ease, stroke 0.5s ease; +} + +.essentials-visual-content.active .db-conn { + opacity: 1; +} + +.db-connections-overlay .db-conn { + animation: db-conn-pulse-stagger 4s ease-in-out infinite; +} + +.db-connections-overlay .db-conn-1 { + animation-delay: 0s; +} + +.db-connections-overlay .db-conn-2 { + animation-delay: 1s; +} + +.db-connections-overlay .db-conn-3 { + animation-delay: 2s; +} + +.db-connections-overlay .db-conn-4 { + animation-delay: 3s; +} + +.database-item-centered.db-3.animate ~ .db-connections-overlay .db-conn-3 { + animation: db-conn-failure 8s ease-in-out 3s infinite; +} + +@keyframes db-conn-pulse-stagger { + 0%, 15%, 100% { + opacity: 0; + } + 5%, 10% { + opacity: 0.9; + } +} + +@keyframes db-conn-failure { + 0%, 100% { + opacity: 0; + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + } + 12.5%, 37.5% { + opacity: 1; + stroke: rgba(239, 68, 68, 0.95); + stroke-width: 4; + } +} + +.database-item { + position: relative; + display: flex; + flex-direction: column; + align-items: center; +} + +.database-item svg { + position: relative; + z-index: 2; +} + +.database-item .db-stroke { + stroke: rgba(44, 224, 191, 0.8); + fill: none; + transition: stroke 0.5s ease; +} + +.database-item .db-connection { + position: absolute; + top: -40px; + left: 50%; + transform: translateX(-50%); + stroke: rgba(44, 224, 191, 0.5); + stroke-width: 2; + stroke-dasharray: 4, 4; + opacity: 0; + transition: opacity 0.3s ease, stroke 0.5s ease; +} + +.database-item.animate .db-connection { + opacity: 1; + animation: db-connection-pulse 3s ease-in-out infinite; +} + +.database-item.db-4.animate { + animation: db-failure-recovery 8s ease-in-out 3s infinite; +} + +.database-item.db-4.animate .db-stroke { + animation: db-stroke-failure-recovery 8s ease-in-out 3s infinite; +} + +.database-item.db-4.animate .db-connection { + animation: db-connection-failure 8s ease-in-out 3s infinite; +} + +@keyframes db-connection-pulse { + 0%, 100% { + opacity: 0.5; + } + 50% { + opacity: 0.95; + } +} + +@keyframes db-failure-recovery { + 0%, 100% { + transform: scale(1); + } + 25% { + transform: scale(1.15); + } + 50% { + transform: scale(1); + } +} + +@keyframes db-stroke-failure-recovery { + 0%, 100% { + stroke: rgba(44, 224, 191, 0.8); + } + 12.5%, 37.5% { + stroke: rgba(239, 68, 68, 0.9); + } +} + +@keyframes db-connection-failure { + 0%, 100% { + opacity: 0.5; + stroke: rgba(44, 224, 191, 0.6); + stroke-width: 2.5; + } + 12.5%, 37.5% { + opacity: 1; + stroke: rgba(239, 68, 68, 0.95); + stroke-width: 4; + } +} + +/* Providers section - reverse layout with darker teal */ +.providers-section { + padding: 67px 0 0 0; + background: linear-gradient(225deg, rgba(7, 131, 143, 0.12) 0%, rgba(17, 112, 124, 0.08) 40%, rgba(2, 65, 76, 0.15) 100%); +} + +[data-theme='dark'] .providers-section { + background: linear-gradient(225deg, rgba(45, 212, 191, 0.06) 0%, rgba(7, 131, 143, 0.08) 50%, rgba(2, 41, 49, 0.12) 100%); +} + +.providers-section .unified { + min-height: 336px; +} + +.providers-section .sidebar { + align-items: flex-end; +} + +/* ===== Unified Section Header Styles ===== */ +.container { + max-width: 1400px; + margin: 0 auto; + padding: 0 24px; +} + +.unified { + display: grid; + grid-template-columns: 1fr; + gap: 40px; + min-height: 480px; +} + +.providers-section .unified { + min-height: 336px; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 20px; +} + +.visual { + position: relative; + min-height: 440px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16px; + padding: 48px 32px; + overflow: visible; +} + +.providers-section .visual { + min-height: 308px; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + gap: 24px; +} + +.section-header-content { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; +} + +.section-header-right { + flex-direction: row-reverse; +} + +.section-header-right .section-header-content { + align-items: flex-end; +} + +.section-main-title { + font-size: 2.5rem; + font-weight: 800; + color: var(--teal-6); + margin: 0; + line-height: 1; + letter-spacing: 0.02em; + white-space: nowrap; +} + +.section-subtitle { + font-size: 1rem; + color: var(--teal-10); + margin: 0.5rem 0 0 0; + font-weight: 400; + line-height: 1.4; +} + +/* Section 1, 2 & 3 - same darker teal */ +.essentials-section .section-main-title, +.providers-section .section-main-title, +.anywhere-section .section-main-title { + color: var(--teal-7); +} + +[data-theme='dark'] .section-main-title { + color: #2dd4bf; +} + +[data-theme='dark'] .section-subtitle { + color: var(--teal-4); +} + +[data-theme='dark'] .essentials-section .section-main-title, +[data-theme='dark'] .providers-section .section-main-title, +[data-theme='dark'] .anywhere-section .section-main-title { + color: var(--teal-6); +} + +.section-header-cta { + font-size: 0.9rem; + color: var(--teal-6); + text-decoration: none; + white-space: nowrap; + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.section-header-cta:hover { + opacity: 1; + text-decoration: underline; +} + +.section-header-right-col { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; +} + +[data-theme='dark'] .section-header-cta { + color: #2dd4bf; +} + +/* ===== Navigation Dots ===== */ +.section-nav-dots { + display: flex; + flex-direction: row; + gap: 32px; + align-items: center; + position: relative; +} + +.nav-dot { + display: flex; + align-items: center; + gap: 0; + background: transparent; + border: none; + padding: 0 0 8px 0; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.9rem; + color: var(--teal-6); + white-space: nowrap; + font-weight: 400; + position: relative; +} + +.nav-dot-progress { + position: absolute; + bottom: 0; + left: 0; + height: 2px; + background: var(--teal-7); + transition: width 0.05s linear; + pointer-events: none; +} + +.nav-dot::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--teal-7); + opacity: 0.15; +} + +.nav-dot:hover { + opacity: 0.7; +} + +.nav-dot.active { + font-weight: 700; +} + +/* All sections - same darker teal */ +.essentials-section .nav-dot, +.providers-section .nav-dot, +.anywhere-section .nav-dot { + color: var(--teal-7); +} + +.essentials-section .nav-dot::after, +.providers-section .nav-dot::after, +.anywhere-section .nav-dot::after, +.essentials-section .nav-dot-progress, +.providers-section .nav-dot-progress, +.anywhere-section .nav-dot-progress { + background: var(--teal-7); +} + +[data-theme='dark'] .nav-dot { + color: #2dd4bf; +} + +[data-theme='dark'] .nav-dot::after, +[data-theme='dark'] .nav-dot-progress { + background: #2dd4bf; +} + +[data-theme='dark'] .nav-dot.active { + color: #2dd4bf; +} + +[data-theme='dark'] .essentials-section .nav-dot, +[data-theme='dark'] .providers-section .nav-dot, +[data-theme='dark'] .anywhere-section .nav-dot { + color: var(--teal-6); +} + +/* ===== Unified Section Text Content ===== */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + gap: 24px; +} + +.section-header-content { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; +} + +.section-header-right { + flex-direction: row-reverse; +} + +.section-header-right .section-header-content { + align-items: flex-end; +} + +.section-main-title { + font-size: 2.5rem; + font-weight: 800; + color: var(--teal-6); + margin: 0; + line-height: 1; + letter-spacing: 0.02em; + white-space: nowrap; +} + +.section-subtitle { + font-size: 1rem; + color: var(--teal-10); + margin: 0.5rem 0 0 0; + font-weight: 400; + line-height: 1.4; +} + +/* Section 1, 2 & 3 - same darker teal */ +.essentials-section .section-main-title, +.providers-section .section-main-title, +.anywhere-section .section-main-title { + color: var(--teal-7); +} + +[data-theme='dark'] .section-main-title { + color: #2dd4bf; +} + +[data-theme='dark'] .section-subtitle { + color: var(--teal-4); +} + +[data-theme='dark'] .essentials-section .section-main-title, +[data-theme='dark'] .providers-section .section-main-title, +[data-theme='dark'] .anywhere-section .section-main-title { + color: var(--teal-6); +} + +.section-header-cta { + font-size: 0.9rem; + color: var(--teal-6); + text-decoration: none; + white-space: nowrap; + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.section-header-cta:hover { + opacity: 1; + text-decoration: underline; +} + +.section-header-right-col { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; +} + +[data-theme='dark'] .section-header-cta { + color: #2dd4bf; +} + +/* ===== Unified Section Text Content ===== */ +.section-text-content { + max-width: 100%; + margin: 0 0 48px 0; + position: relative; + min-height: 140px; + text-align: left; + padding: 0; +} + +/* Section 2 text aligned right */ +.section-text-content-right { + text-align: right; +} + +.section-text-item { + position: absolute; + width: 100%; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.3s ease, transform 0.3s ease; +} + +.section-text-item.active { + opacity: 1; + transform: translateY(0); +} + +.section-text-item h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 16px; + color: var(--teal-6); +} + +[data-theme='dark'] .section-text-item h3 { + color: #2dd4bf; +} + +.section-text-item p { + color: var(--lp-c-text-2); + line-height: 1.7; + font-size: 1.05rem; +} + +.providers-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 24px; +} + +.providers-unified { + display: grid; + grid-template-columns: 1fr 350px; + gap: 40px; + min-height: 600px; +} + +.providers-sidebar { + display: flex; + flex-direction: column; + gap: 20px; + align-items: flex-end; + text-align: right; +} + +.providers-title { + font-size: 2rem; + font-weight: 700; + color: #11707C; + margin-bottom: 12px; + line-height: 1.3; + text-align: right; +} + +.providers-cta-link { + text-align: right; +} + +[data-theme='dark'] .providers-title { + color: #2dd4bf; +} + +.providers-buttons { + display: flex; + flex-direction: column; + gap: 12px; +} + +.providers-nav-button { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: rgba(17, 112, 124, 0.05); + border: 2px solid rgba(17, 112, 124, 0.15); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 1rem; + font-weight: 500; + color: var(--lp-c-text-1); +} + +.providers-nav-button:hover { + background: rgba(17, 112, 124, 0.1); + border-color: rgba(17, 112, 124, 0.3); + transform: translateX(-4px); +} + +.providers-nav-button.active { + background: rgba(17, 112, 124, 0.15); + border-color: #11707C; + color: #11707C; +} + +[data-theme='dark'] .providers-nav-button.active { + color: #2dd4bf; + border-color: #2dd4bf; +} + +.providers-nav-button svg { + flex-shrink: 0; +} + +.providers-content-flip { + position: relative; + min-height: 200px; + margin-top: 20px; + width: 100%; +} + +.providers-content-item { + position: absolute; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.3s ease, transform 0.3s ease; + width: 100%; + text-align: right; +} + +.providers-content-item.active { + opacity: 1; + transform: translateY(0); +} + +.providers-content-item h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 12px; + color: #11707C; + text-align: right; +} + +[data-theme='dark'] .providers-content-item h3 { + color: #2dd4bf; +} + +.providers-content-item p { + color: var(--lp-c-text-2); + line-height: 1.6; + font-size: 1rem; + text-align: right; +} + +.providers-visual-unified { + position: relative; + min-height: 440px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16px; + padding: 48px 32px; +} + +[data-theme='dark'] .providers-visual-unified { + background: transparent; +} + +.providers-cp { + position: absolute; + width: 160px; + height: auto; + opacity: 1; + filter: drop-shadow(0 6px 24px rgba(17, 112, 124, 0.4)); + transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +.providers-cp-top { + left: 50%; + top: 15%; + transform: translate(-50%, 0); +} + +.providers-cp-bottom-left { + left: 20%; + bottom: 18%; + transform: translate(0, 0); +} + +.providers-cp-bottom-right { + right: 20%; + bottom: 18%; + transform: translate(0, 0); +} + +/* Control plane movement for different features */ +/* Onboarding API feature - triangle formation (default) */ +.providers-cp-1.pos-onboarding-1 { + left: 50%; + top: 15%; + transform: translate(-50%, 0); +} + +.providers-cp-2.pos-onboarding-2 { + left: 20%; + bottom: 18%; + top: auto; + right: auto; + transform: translate(0, 0); +} + +.providers-cp-3.pos-onboarding-3 { + right: 20%; + bottom: 18%; + top: auto; + left: auto; + transform: translate(0, 0); +} + +/* Shared tooling feature - spread out more */ +.providers-cp-1.pos-tooling-1 { + left: 50%; + top: 10%; + transform: translate(-50%, 0); +} + +.providers-cp-2.pos-tooling-2 { + left: 15%; + bottom: 15%; + top: auto; + right: auto; + transform: translate(0, 0); +} + +.providers-cp-3.pos-tooling-3 { + right: 15%; + bottom: 15%; + top: auto; + left: auto; + transform: translate(0, 0); +} + +/* Observability feature - horizontal line */ +.providers-cp-1.pos-obs-1 { + left: 15%; + top: 50%; + bottom: auto; + right: auto; + transform: translateY(-50%); +} + +.providers-cp-2.pos-obs-2 { + left: 50%; + top: 50%; + bottom: auto; + right: auto; + transform: translate(-50%, -50%); +} + +.providers-cp-3.pos-obs-3 { + right: 15%; + top: 50%; + bottom: auto; + left: auto; + transform: translateY(-50%); +} + +/* Providers vault visualization */ +.providers-vault-container { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; +} + +.providers-vault-container.animate { + opacity: 1; +} + +.providers-vault-icon { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 10; +} + +.providers-vault-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.providers-vault-line { + stroke: rgba(147, 51, 234, 0.3); + stroke-width: 2; + stroke-dasharray: 3, 3; + opacity: 0; +} + +.providers-vault-container.animate .providers-vault-line { + opacity: 1; + animation: connection-pulse 3s ease-in-out infinite; +} + +.providers-vault-container.animate .providers-vault-line-top { + animation-delay: 0s; +} + +.providers-vault-container.animate .providers-vault-line-bl { + animation-delay: 0.3s; +} + +.providers-vault-container.animate .providers-vault-line-br { + animation-delay: 0.6s; +} + +/* Providers onboarding API visualization */ +.providers-onboarding-container { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; +} + +.providers-onboarding-container.animate { + opacity: 1; +} + +.providers-onboarding-icon { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 10; +} + +.providers-onboarding-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.providers-onboarding-line { + stroke: rgba(147, 51, 234, 0.3); + stroke-width: 2; + stroke-dasharray: 3, 3; + opacity: 0; +} + +.providers-onboarding-container.animate .providers-onboarding-line { + opacity: 1; + animation: connection-pulse 3s ease-in-out infinite; +} + +.providers-onboarding-container.animate .providers-onboarding-line-top { + animation-delay: 0s; +} + +.providers-onboarding-container.animate .providers-onboarding-line-bl { + animation-delay: 0.3s; +} + +.providers-onboarding-container.animate .providers-onboarding-line-br { + animation-delay: 0.6s; +} + +/* Providers tooling visualization */ +.providers-tooling-container { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; +} + +.providers-tooling-container.animate { + opacity: 1; +} + +.providers-vault-icon { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 10; + filter: drop-shadow(0 4px 12px rgba(147, 51, 234, 0.4)); +} + +.providers-vault-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.providers-vault-line { + stroke: rgba(147, 51, 234, 0.4); + stroke-width: 3; + stroke-dasharray: 5, 5; + opacity: 0; +} + +.providers-tooling-container.animate .providers-vault-line { + opacity: 1; + animation: connection-pulse 3s ease-in-out infinite; +} + +.providers-tooling-container.animate .providers-vault-line-top { + animation-delay: 0s; +} + +.providers-tooling-container.animate .providers-vault-line-bl { + animation-delay: 0.3s; +} + +.providers-tooling-container.animate .providers-vault-line-br { + animation-delay: 0.6s; +} + +/* Providers policy visualization */ +.providers-policy-container { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; +} + +.providers-policy-container.animate { + opacity: 1; +} + +.providers-policy-icon { + position: absolute; + z-index: 10; +} + +.providers-policy-top { + left: 50%; + top: 8%; + transform: translate(-50%, 0); +} + +.providers-policy-bl { + left: 15%; + bottom: 10%; + transform: translate(0, 0); +} + +.providers-policy-br { + right: 15%; + bottom: 10%; + transform: translate(0, 0); +} + +.providers-policy-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.providers-policy-line { + stroke: rgba(147, 51, 234, 0.3); + stroke-width: 2; + stroke-dasharray: 3, 3; + opacity: 0; +} + +.providers-policy-container.animate .providers-policy-line { + opacity: 1; + animation: connection-pulse 3s ease-in-out infinite; +} + +.providers-policy-container.animate .providers-policy-conn-top { + animation-delay: 0s; +} + +.providers-policy-container.animate .providers-policy-conn-bl { + animation-delay: 0.3s; +} + +.providers-policy-container.animate .providers-policy-conn-br { + animation-delay: 0.6s; +} + +/* Providers observability visualization */ +.providers-observability-container { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; +} + +.providers-observability-container.animate { + opacity: 1; +} + +/* Anywhere section - same as providers but potentially different color */ +.anywhere-section { + padding: 96px 0 48px 0; + background: linear-gradient(180deg, rgba(135, 206, 235, 0.1) 0%, rgba(7, 131, 143, 0.08) 30%, rgba(14, 89, 99, 0.12) 70%, rgba(2, 65, 76, 0.15) 100%); +} + +[data-theme='dark'] .anywhere-section { + background: linear-gradient(180deg, rgba(4, 159, 154, 0.08) 0%, rgba(7, 131, 143, 0.1) 40%, rgba(2, 41, 49, 0.15) 100%); +} + +.anywhere-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 24px; +} + +.anywhere-unified { + display: grid; + grid-template-columns: 350px 1fr; + gap: 40px; + min-height: 600px; +} + +.anywhere-sidebar { + display: flex; + flex-direction: column; + gap: 20px; + align-items: flex-start; + text-align: left; +} + +.anywhere-title { + font-size: 2rem; + font-weight: 700; + color: #0e5963; + margin-bottom: 24px; + line-height: 1.3; + text-align: left; +} + +[data-theme='dark'] .anywhere-title { + color: #2dd4bf; +} + +.anywhere-buttons { + display: flex; + flex-direction: column; + gap: 12px; +} + +.anywhere-content-horizontal { + display: flex; + gap: 20px; + margin-top: 20px; +} + +.anywhere-content-card { + flex: 1; + padding: 24px; + background: rgba(14, 89, 99, 0.03); + border: 2px solid rgba(14, 89, 99, 0.1); + border-radius: 12px; + transition: all 0.3s ease; + opacity: 0.5; + transform: translateY(10px); +} + +.anywhere-content-card.active { + opacity: 1; + transform: translateY(0); + background: rgba(14, 89, 99, 0.08); + border-color: rgba(14, 89, 99, 0.25); +} + +[data-theme='dark'] .anywhere-content-card { + background: rgba(14, 89, 99, 0.08); + border-color: rgba(14, 89, 99, 0.15); +} + +[data-theme='dark'] .anywhere-content-card.active { + background: rgba(14, 89, 99, 0.15); + border-color: rgba(45, 212, 191, 0.3); +} + +.anywhere-content-card svg { + color: #0e5963; + margin-bottom: 16px; +} + +[data-theme='dark'] .anywhere-content-card svg { + color: #2dd4bf; +} + +.anywhere-content-card h3 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 8px; + color: #0e5963; +} + +[data-theme='dark'] .anywhere-content-card h3 { + color: #2dd4bf; +} + +.anywhere-content-card p { + color: var(--lp-c-text-2); + line-height: 1.6; + font-size: 0.95rem; + margin: 0; +} + +.new-badge-inline { + display: inline-block; + padding: 2px 8px; + background: rgba(4, 159, 154, 0.15); + color: var(--teal-6); + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + margin-left: 6px; +} + +[data-theme='dark'] .new-badge-inline { + background: rgba(45, 212, 191, 0.2); + color: #2dd4bf; +} + +.anywhere-nav-button { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: rgba(14, 89, 99, 0.05); + border: 2px solid rgba(14, 89, 99, 0.15); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 1rem; + font-weight: 500; + color: var(--lp-c-text-1); +} + +.anywhere-nav-button:hover { + background: rgba(14, 89, 99, 0.1); + border-color: rgba(14, 89, 99, 0.3); + transform: translateX(-4px); +} + +.anywhere-nav-button.active { + background: rgba(14, 89, 99, 0.15); + border-color: #0e5963; + color: #0e5963; +} + +[data-theme='dark'] .anywhere-nav-button.active { + color: #2dd4bf; + border-color: #2dd4bf; +} + +.anywhere-nav-button svg { + flex-shrink: 0; +} + +.anywhere-content-flip { + position: relative; + min-height: 200px; + margin-top: 20px; +} + +.anywhere-content-item { + position: absolute; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.3s ease, transform 0.3s ease; +} + +.anywhere-content-item.active { + opacity: 1; + transform: translateY(0); +} + +.anywhere-content-item h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 12px; + color: #0e5963; +} + +[data-theme='dark'] .anywhere-content-item h3 { + color: #2dd4bf; +} + +.anywhere-content-item p { + color: var(--lp-c-text-2); + line-height: 1.6; + font-size: 1rem; +} + +.anywhere-visual-unified { + position: relative; + min-height: 440px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16px; + padding: 48px 32px; +} + +[data-theme='dark'] .anywhere-visual-unified { + background: transparent; +} + +.anywhere-cp { + position: absolute; + width: 200px; + height: auto; + opacity: 0; + filter: drop-shadow(0 6px 24px rgba(14, 89, 99, 0.4)); + transition: opacity 0.5s ease; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.anywhere-cp.active { + opacity: 1; +} + +.anywhere-cp-1 { + /* Gardener */ +} + +.anywhere-cp-2 { + /* Sovereign clouds */ +} + +.anywhere-cp-3 { + /* Kind */ +} + +/* Anywhere feature containers */ +.anywhere-feature-container { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + transition: opacity 0.5s ease; + pointer-events: none; + overflow: visible; +} + +.anywhere-feature-container.active { + opacity: 1; + pointer-events: auto; +} + +.anywhere-hangar-gardener { + position: absolute; + left: 50%; + top: 25%; + transform: translate(-50%, -20%); + + height: 154px !important; + filter: drop-shadow(0 6px 24px rgba(14, 89, 99, 0.4)); +} + +.anywhere-hangar-kind { + position: absolute; + left: 50%; + top: 35%; + transform: translate(-50%, -30%); + height: 174px !important; + filter: drop-shadow(0 6px 24px rgba(14, 89, 99, 0.4)); +} + +/* Badge styles */ +.anywhere-badge { + position: absolute; + padding: 8px 18px; + border-radius: 8px; + font-size: 0.85rem; + font-weight: 700; + color: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + animation: badge-pop-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; + opacity: 0; + transition: transform 0.2s ease; +} + +.anywhere-badge:hover { + transform: translateY(-4px) scale(1.05); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); +} + +@keyframes badge-pop-in { + from { + opacity: 0; + transform: translateY(-30px) scale(0.5); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* AWS - orange */ +.anywhere-badge-aws { + background: linear-gradient(135deg, #FF9900 0%, #FF7700 100%); + bottom: 8%; + left: 15%; + animation-delay: 0.2s; +} + +/* Azure - blue */ +.anywhere-badge-azure { + background: linear-gradient(135deg, #0078D4 0%, #0063B1 100%); + bottom: 8%; + left: 35%; + animation-delay: 0.3s; +} + +/* GCP - subtle blue/white design */ +.anywhere-badge-gcp { + background: linear-gradient(135deg, #4285F4 0%, #5A9FED 100%); + color: white; + box-shadow: 0 4px 12px rgba(66, 133, 244, 0.3); + bottom: 8%; + left: 55%; + animation-delay: 0.4s; +} + +/* Other - gray */ +.anywhere-badge-other { + background: linear-gradient(135deg, #6B7280 0%, #4B5563 100%); + bottom: 8%; + left: 75%; + animation-delay: 0.5s; +} + +/* EU badges */ +.anywhere-badge-eu-pub-dev { + background: linear-gradient(135deg, #003399 0%, #002266 100%); + bottom: 8%; + left: 20%; + animation-delay: 0.2s; +} + +.anywhere-badge-eu-pub-live { + background: linear-gradient(135deg, #87CEEB 0%, #5DADE2 100%); + color: #003399; + bottom: 8%; + left: 45%; + animation-delay: 0.3s; +} + +.anywhere-badge-eu-gov-live { + background: linear-gradient(135deg, #7C3AED 0%, #6D28D9 100%); + bottom: 8%; + left: 70%; + animation-delay: 0.4s; +} + +/* Connection lines */ +.anywhere-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.anywhere-conn-line { + stroke: rgba(7, 131, 143, 0.2); + stroke-width: 1.5; + stroke-linecap: round; + animation: line-subtle-flow 3s ease-in-out infinite; + stroke-dasharray: 8, 8; + opacity: 0; +} + +@keyframes line-subtle-flow { + 0%, 100% { + opacity: 0.3; + stroke-dashoffset: 0; + } + 50% { + opacity: 0.6; + stroke-dashoffset: 16; + } +} + +.anywhere-conn-aws { + animation-delay: 0s; +} + +.anywhere-conn-azure { + animation-delay: 0.5s; +} + +.anywhere-conn-gcp { + animation-delay: 1s; +} + +.anywhere-conn-other { + animation-delay: 1.5s; +} + +.anywhere-conn-eu-1 { + animation-delay: 0s; +} + +.anywhere-conn-eu-2 { + animation-delay: 0.7s; +} + +.anywhere-conn-eu-3 { + animation-delay: 1.4s; +} + +/* Flying control planes */ +.anywhere-flying-cp { + position: absolute; + width: 150px; + height: auto; + opacity: 0; + left: 50%; + top: 28%; + transform: translateX(-50%); + filter: drop-shadow(0 4px 12px rgba(14, 89, 99, 0.3)); + pointer-events: none; +} + +@keyframes fly-out { + 0% { + opacity: 0; + top: 28%; + transform: translate(-50%, 0) scale(0.4) rotate(0deg); + } + 10% { + opacity: 1; + top: 22%; + transform: translate(-50%, 0) scale(0.6) rotate(-2deg); + } + 30% { + opacity: 1; + top: 12%; + transform: translate(-50%, 0) scale(0.8) rotate(2deg); + } + 60% { + opacity: 0.8; + top: -3%; + transform: translate(-50%, 0) scale(1) rotate(-1deg); + } + 100% { + opacity: 0; + top: -20%; + transform: translate(-50%, 0) scale(1.2) rotate(0deg); + } +} + +.anywhere-feature-container.active .anywhere-flying-cp-1 { + animation: fly-out 8s ease-out infinite; + animation-delay: 0s; + left: 48%; +} + +.anywhere-feature-container.active .anywhere-flying-cp-2 { + animation: fly-out 8s ease-out infinite; + animation-delay: 4s; + left: 52%; +} + +/* Kind control planes - stay and move around */ +.kind-cp { + position: absolute; + width: 120px; + height: auto; + opacity: 0; + filter: drop-shadow(0 4px 12px rgba(14, 89, 99, 0.3)); + pointer-events: none; +} + +@keyframes kind-float-1 { + 0%, 100% { + transform: translate(0, 0) rotate(0deg); + } + 25% { + transform: translate(30px, -20px) rotate(3deg); + } + 50% { + transform: translate(60px, 10px) rotate(-2deg); + } + 75% { + transform: translate(20px, 30px) rotate(1deg); + } +} + +@keyframes kind-float-2 { + 0%, 100% { + transform: translate(0, 0) rotate(0deg); + } + 25% { + transform: translate(-40px, 20px) rotate(-3deg); + } + 50% { + transform: translate(-70px, -15px) rotate(2deg); + } + 75% { + transform: translate(-30px, -35px) rotate(-1deg); + } +} + +.anywhere-feature-container.active .kind-cp-1 { + left: 40%; + top: 18%; + opacity: 1; + animation: kind-float-1 12s ease-in-out infinite; +} + +.anywhere-feature-container.active .kind-cp-2 { + left: 60%; + top: 25%; + opacity: 1; + animation: kind-float-2 15s ease-in-out infinite; + animation-delay: 2s; +} + +/* Replication visualization */ +.essentials-replication-demo { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 40px; + position: relative; +} + +.gitops-description { + max-width: 500px; + text-align: center; + color: var(--teal-10); + font-size: 0.95rem; + line-height: 1.6; + margin: 20px 0 0 0; +} + +[data-theme='dark'] .gitops-description { + color: var(--teal-2); +} + +.demo-cp-large { + filter: drop-shadow(0 6px 24px rgba(4, 159, 154, 0.4)); + animation: demo-cp-float 4s ease-in-out infinite; +} + +.replication-clouds { + position: relative; + display: flex; + justify-content: center; + align-items: center; + gap: 0; +} + +.cloud-single { + position: absolute; + transition: all 0.6s ease-out; +} + +.cloud-single.cloud-left { + left: 50%; + transform: translateX(-50%); + opacity: 1; +} + +.cloud-single.cloud-right { + left: 50%; + transform: translateX(-50%); + opacity: 0; +} + +.replication-clouds.animate .cloud-single.cloud-left { + animation: cloud-slide-left 0.6s ease-out 1s forwards; +} + +.replication-clouds.animate .cloud-single.cloud-right { + animation: cloud-slide-right 0.6s ease-out 1s forwards; +} + +@keyframes cloud-slide-left { + from { + left: 50%; + transform: translateX(-50%); + } + to { + left: 0; + transform: translateX(0); + } +} + +@keyframes cloud-slide-right { + from { + left: 50%; + transform: translateX(-50%); + opacity: 0; + } + to { + left: 100%; + transform: translateX(-100%); + opacity: 1; + } +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.essentials-features { + display: flex; + flex-direction: column; + gap: 16px; +} + +.essentials-features-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.essentials-feature { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 18px; + background: rgba(4, 159, 154, 0.05); + border-radius: 12px; + border: 1px solid rgba(4, 159, 154, 0.1); + transition: all 0.3s ease; + font-size: 1rem; + font-weight: 500; + color: var(--teal-10); +} + +[data-theme='dark'] .essentials-feature { + background: rgba(44, 224, 191, 0.08); + border: 1px solid rgba(44, 224, 191, 0.15); + color: var(--teal-2); +} + +.essentials-feature:hover { + transform: translateX(4px); + background: rgba(4, 159, 154, 0.12); + border-color: rgba(4, 159, 154, 0.3); +} + +[data-theme='dark'] .essentials-feature:hover { + background: rgba(44, 224, 191, 0.15); + border-color: rgba(44, 224, 191, 0.35); +} + +.essentials-feature svg { + flex-shrink: 0; + color: var(--teal-6); +} + +[data-theme='dark'] .essentials-feature svg { + color: var(--teal-4); +} + +.essentials-feature span { + line-height: 1.4; +} + +.essentials-feature-new { + position: relative; +} + +.new-badge { + display: inline-block; + padding: 2px 8px; + background: linear-gradient(135deg, var(--teal-4) 0%, var(--teal-6) 100%); + color: white; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 8px; + vertical-align: middle; +} + +@media (max-width: 996px) { + .essentials-section { + padding: 80px 20px 60px; + } + + .essentials-card { + padding: 40px 30px; + gap: 30px; + } + + .essentials-card-interactive { + min-height: 500px; + } + + .essentials-unified { + grid-template-columns: 1fr; + gap: 30px; + } + + .essentials-buttons { + flex-direction: row; + overflow-x: auto; + } + + .essentials-nav-button { + white-space: nowrap; + font-size: 0.9rem; + padding: 14px 16px; + } + + .essentials-content-flip { + min-height: 180px; + } + + .essentials-content-item h3 { + font-size: 1.5rem; + } + + .essentials-content-item p { + font-size: 0.95rem; + } + + .essentials-visual-unified { + min-height: 500px; + padding: 40px 30px; + } + + .unified-cp { + width: 220px; + } + + .yaml-left-container { + left: 20px; + max-width: 250px; + } + + .yaml-right-container { + right: 40px; + } + + .essentials-section-item { + margin-bottom: 60px; + } + + .essentials-content-wrapper h3 { + font-size: 1.6rem; + } + + .essentials-content-wrapper p { + font-size: 1rem; + } + + .essentials-visual-standalone { + min-height: 450px; + padding: 40px 30px; + } + + .standalone-cp { + width: 220px; + } + + .essentials-visual-large { + min-height: 450px; + padding: 40px 30px; + } + + .essentials-descriptions { + padding: 30px 40px; + } + + .essentials-features-elegant { + grid-template-columns: 1fr; + gap: 40px; + margin-top: 40px; + padding: 0 10px; + } + + .essentials-tab { + font-size: 0.95rem; + padding: 18px 20px; + } + + .essentials-card-number { + font-size: 4rem; + } + + .essentials-card-content h3 { + font-size: 1.5rem; + } + + .essentials-interactive { + grid-template-columns: 1fr; + gap: 40px; + } + + .essentials-visual { + min-height: 350px; + } + + .essentials-features-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .essentials-section { + padding: 60px 16px 40px; + } + + .essentials-container { + gap: 40px; + } + + .essentials-title { + font-size: 2.2rem; + } + + .essentials-card { + flex-direction: column; + padding: 30px 24px; + gap: 20px; + } + + .essentials-card-interactive { + min-height: auto; + } + + .essentials-unified { + grid-template-columns: 1fr; + gap: 20px; + } + + .essentials-sidebar { + width: 100%; + } + + .essentials-buttons { + gap: 8px; + } + + .essentials-nav-button { + font-size: 0.85rem; + padding: 12px 14px; + } + + .essentials-nav-button svg { + width: 16px; + height: 16px; + } + + .essentials-content-flip { + min-height: 140px; + margin-top: 16px; + } + + .essentials-content-item h3 { + font-size: 1.2rem; + } + + .essentials-content-item p { + font-size: 0.85rem; + line-height: 1.5; + } + + .essentials-visual-unified { + min-height: 380px; + padding: 30px 12px; + width: 100%; + overflow: hidden; + } + + .unified-cp { + width: 120px; + } + + .yaml-left-container { + left: 5px; + max-width: 110px; + } + + .essentials-yaml-unified { + font-size: 0.45rem; + padding: 6px; + line-height: 1.2; + } + + .yaml-right-container { + right: 10px; + } + + .yaml-cloud-right { + width: 140px; + height: 105px; + } + + .gitops-animation-area { + width: 100%; + max-width: 320px; + } + + .gitops-cloud-wrapper svg { + width: 110px; + height: 80px; + } + + .gitops-conn-left, + .gitops-conn-right { + display: none; + } + + .selfhealing-animation-area { + width: 100%; + } + + .healing-resource-icon { + width: 50px; + height: 50px; + border-radius: 12px; + } + + .healing-resource-icon svg { + width: 28px; + height: 28px; + } + + .healing-resource-1 { + top: 8%; + } + + .healing-resource-2 { + right: 8%; + } + + .healing-resource-3 { + bottom: 8%; + } + + .healing-resource-4 { + left: 8%; + } + + .healing-conn-line-simple { + display: none; + } + + .essentials-section-item { + margin-bottom: 50px; + } + + .essentials-content-wrapper { + margin-bottom: 30px; + } + + .essentials-content-wrapper h3 { + font-size: 1.4rem; + } + + .essentials-content-wrapper p { + font-size: 0.95rem; + } + + .essentials-visual-standalone { + min-height: 400px; + padding: 40px 24px; + } + + .standalone-cp { + width: 180px; + } + + .essentials-visual-large { + min-height: 400px; + padding: 40px 24px; + } + + .essentials-descriptions { + padding: 30px 24px; + } + + .essentials-description h3 { + font-size: 1.3rem; + } + + .essentials-description p { + font-size: 0.95rem; + } + + .essentials-tabs { + flex-direction: column; + } + + .essentials-tab { + justify-content: flex-start; + padding: 16px 24px; + border-bottom: 1px solid rgba(4, 159, 154, 0.1); + } + + .essentials-tab.active::after { + display: none; + } + + .essentials-tab.active { + border-left: 3px solid var(--lp-c-brand-2); + padding-left: 21px; + } + + .essentials-features-elegant { + grid-template-columns: 1fr; + gap: 30px; + margin-top: 30px; + padding: 0; + } + + .essentials-feature-section h4 { + font-size: 1rem; + } + + .essentials-feature-list-elegant li { + font-size: 0.9rem; + } + + .essentials-card-number { + font-size: 3rem; + align-self: flex-start; + } + + .essentials-card-content h3 { + font-size: 1.4rem; + margin-bottom: 20px; + } + + .essentials-interactive { + grid-template-columns: 1fr; + gap: 30px; + } + + .essentials-visual { + min-height: 300px; + } + + .essentials-cp-cloud-demo { + transform: scale(0.8); + } + + .essentials-replication-demo { + transform: scale(0.8); + } + + .essentials-selfhealing-container { + transform: scale(0.8); + } + + .database-grid { + max-width: 160px; + gap: 15px; + } + + .essentials-yaml { + font-size: 0.75rem; + padding: 16px; + } + + .essentials-feature { + font-size: 0.95rem; + padding: 12px 16px; + } + + .essentials-feature svg { + width: 18px; + height: 18px; + } + + .essentials-feature-item { + font-size: 0.95rem; + padding: 14px 16px; + } + + .essentials-feature-item svg { + width: 18px; + height: 18px; + } +} + +/* ===== Cloud Provider Badges ===== */ +.cloud-providers { + margin-top: 16px; + padding-top: 16px; +} + +.cloud-providers-label { + font-weight: 600; + margin-bottom: 10px; + color: var(--ifm-color-emphasis-800); + font-size: 0.85rem; +} + +.cloud-providers-list { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.provider-badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + background: var(--ifm-color-emphasis-100); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 6px; + color: var(--ifm-color-emphasis-800); + text-decoration: none; + font-size: 0.85rem; + font-weight: 500; + transition: all 0.2s ease; + white-space: nowrap; + line-height: 1; + height: auto; + width: auto; + min-height: 0; + min-width: 0; + flex: 0 0 auto; +} + +.provider-badge:hover { + background: var(--teal-2); + border-color: var(--teal-6); + color: var(--teal-10); + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(4, 159, 154, 0.15); + text-decoration: none; +} diff --git a/src/pages/about/legal-disclosure.md b/src/pages/about/legal-disclosure.md new file mode 100644 index 0000000..8fbf009 --- /dev/null +++ b/src/pages/about/legal-disclosure.md @@ -0,0 +1,28 @@ +--- +title: Legal Disclosure +description: Legal Disclosure / Impressum for Open Control Plane +--- + +# Legal Disclosure / Impressum + +SAP Deutschland SE & Co. KG + +**Hauptsitz / Registered Office:** +SAP Deutschland SE & Co. KG +Hasso-Plattner-Ring 7 +69190 Walldorf + +Telefon: +49/6227/7-47474 +Telefax: +49/6227/7-57575 +info.germany@sap.com + +Sitz der Gesellschaft/Registered Office: Walldorf, Germany +Registergericht/Commercial Register Mannheim HRA 350654 + +**Persönlich haftende Gesellschafterin/General Partner:** SAP SE + +**Vorstand/Executive Board:** Christian Klein (CEO), Muhammad Alam, Dominik Asam, Thomas Saueressig, Sebastian Steinhäuser, Gina Vargiu-Breuer + +**Vorsitzender des Aufsichtsrats/Chairperson of the Supervisory Board:** Pekka Ala-Pietilä + +Registergericht/Commercial Register Mannheim HRB 719915 diff --git a/src/pages/about/privacy.md b/src/pages/about/privacy.md new file mode 100644 index 0000000..ec0eb80 --- /dev/null +++ b/src/pages/about/privacy.md @@ -0,0 +1,66 @@ +--- +title: Privacy Statement +description: Privacy Statement for Open Control Plane +--- + +# Privacy Statement + +## Controller + +SAP Deutschland SE & Co. KG +Hasso-Plattner-Ring 7 +69190 Walldorf, Germany + +Email: [privacy@sap.com](mailto:privacy@sap.com) + +SAP Deutschland SE & Co. KG ("SAP", "we", "us") is the controller responsible for processing your personal data when you visit the Open Control Plane documentation website at [openmcp-project.github.io/docs](https://openmcp-project.github.io/docs). + +## Data We Collect + +### Log Files + +When you visit our website, the web server automatically records log files that may contain the following information: + +- IP address (anonymized) +- Date and time of access +- Requested URL and referrer URL +- Browser type and version +- Operating system + +These log files are stored for a maximum of **7 days** and are used solely for ensuring the security and stability of the website. The legal basis for this processing is our legitimate interest in providing a secure website (Art. 6(1)(f) GDPR). + +### Cookies + +This website uses only **technically necessary cookies** that are required for the website to function properly. No tracking cookies or analytics cookies are used. These cookies do not require your consent under applicable law. + +### GitHub Issues and Contributions + +If you submit issues, pull requests, or other contributions through GitHub, GitHub's own privacy policy applies to the data you provide. We process your GitHub username and contribution content to maintain and improve the Open Control Plane project. + +## Your Rights + +Under the General Data Protection Regulation (GDPR), you have the following rights: + +- **Right of access** (Art. 15 GDPR) — You may request information about your personal data that we process. +- **Right to rectification** (Art. 16 GDPR) — You may request correction of inaccurate personal data. +- **Right to erasure** (Art. 17 GDPR) — You may request deletion of your personal data, subject to legal retention obligations. +- **Right to restriction of processing** (Art. 18 GDPR) — You may request that we restrict the processing of your personal data under certain conditions. +- **Right to data portability** (Art. 20 GDPR) — You may request to receive your personal data in a structured, commonly used, and machine-readable format. +- **Right to object** (Art. 21 GDPR) — You may object to the processing of your personal data based on legitimate interests at any time. + +You also have the right to lodge a complaint with a supervisory authority, in particular in the EU Member State of your habitual residence, place of work, or place of the alleged infringement. + +## Contact + +For questions or requests regarding data protection, please contact: + +**SAP Data Protection Officer** +Email: [privacy@sap.com](mailto:privacy@sap.com) + +SAP Deutschland SE & Co. KG +Hasso-Plattner-Ring 7 +69190 Walldorf, Germany + +## Changes to This Privacy Statement + +We may update this privacy statement from time to time. Any changes will be posted on this page. We encourage you to review this statement periodically. diff --git a/src/pages/about/terms-of-use.md b/src/pages/about/terms-of-use.md new file mode 100644 index 0000000..b85183f --- /dev/null +++ b/src/pages/about/terms-of-use.md @@ -0,0 +1,46 @@ +--- +title: Terms of Use +description: Terms of Use for Open Control Plane +--- + +# Terms of Use + +## Introduction + +This website ([openmcp-project.github.io/docs](https://openmcp-project.github.io/docs)) is maintained by Linux Foundation Europe as part of the Open Control Plane project. By accessing or using this website, you agree to be bound by these Terms of Use. If you do not agree to these terms, please do not use this website. + +## Trademarks + +Open Control Plane and its associated logos and trademarks are trademarks of Linux Foundation Europe. Use of these trademarks must comply with the Linux Foundation Europe [Trademark Usage Guidelines](https://linuxfoundation.eu/policies). You may not use these trademarks without prior written permission, except as permitted by applicable law. + +All other trademarks, service marks, and trade names referenced on this site are the property of their respective owners. + +## Copyrights and Licenses + +### Content + +Except where otherwise noted, content on this site is licensed under the [Creative Commons Attribution 4.0 International License (CC-BY 4.0)](https://creativecommons.org/licenses/by/4.0/). You are free to share and adapt the content, provided you give appropriate credit, provide a link to the license, and indicate if changes were made. + +### Software + +Software source code published by the Open Control Plane project is licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0), unless otherwise noted in the respective repository. You may obtain a copy of the license at the link above. + +## Disclaimer of Warranties + +This website and its content are provided on an "AS IS" and "AS AVAILABLE" basis, without warranties of any kind, either express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, or accuracy. Linux Foundation Europe does not warrant that the website will be uninterrupted, error-free, or free of harmful components. + +## Limitation of Liability + +To the fullest extent permitted by applicable law, Linux Foundation Europe, its directors, officers, employees, and agents shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenue, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses, resulting from your access to or use of (or inability to access or use) this website or its content. + +## Privacy + +Your use of this website is also subject to our [Privacy Statement](/about/privacy). Please review it to understand how we collect and use information. + +## General + +These Terms of Use are governed by the laws of Belgium, without regard to conflict of law principles. Linux Foundation Europe reserves the right to modify these terms at any time. Changes will be posted on this page with an updated effective date. Your continued use of the website after such changes constitutes acceptance of the revised terms. + +If any provision of these Terms of Use is found to be unenforceable, the remaining provisions will continue in full force and effect. + +If you have questions about these terms, please contact the Open Control Plane project through [GitHub](https://github.com/openmcp-project). diff --git a/src/pages/index.js b/src/pages/index.js new file mode 100644 index 0000000..d3a2f79 --- /dev/null +++ b/src/pages/index.js @@ -0,0 +1,1233 @@ +/* eslint-disable import/no-unresolved */ +import React, { useEffect, useRef, useState } from "react"; +import clsx from "clsx"; +import Layout from "@theme/Layout"; +import ThemedImage from "@theme/ThemedImage"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; + +export default function Home() { + const { siteConfig } = useDocusaurusContext(); + const [activeFeature, setActiveFeature] = useState(0); + const [activeProviderFeature, setActiveProviderFeature] = useState(0); + const [activeAnywhereFeature, setActiveAnywhereFeature] = useState(0); + const [scrollProgress, setScrollProgress] = useState(0); + const [providerScrollProgress, setProviderScrollProgress] = useState(0); + const [anywhereScrollProgress, setAnywhereScrollProgress] = useState(0); + const section1Ref = useRef(null); + const section2Ref = useRef(null); + const section3Ref = useRef(null); + + useEffect(() => { + const innersourceText = document.getElementsByClassName("typing-open-source")[0]; + const essentialCards = document.querySelectorAll(".essentials-card"); + + const onScroll = () => { + if (innersourceText && isInViewport(innersourceText)) { + innersourceText.classList.add("animate"); + } + + essentialCards.forEach((card) => { + if (isInViewport(card)) { + card.classList.add("visible"); + } + }); + }; + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + // Handle scroll-based feature switching based on main page scroll + useEffect(() => { + const handleMainScroll = () => { + const section = section1Ref.current; + if (!section) return; + + const rect = section.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + const sectionBottom = rect.bottom; + const sectionTop = rect.top; + + // Start when bottom is visible and end when content is still visible + // We need a range where the visual content (images) is fully in view + + // Start: section top at 50% viewport (content becoming centered) + // End: section top at -40% viewport (top exiting but bottom still visible) + const startThreshold = windowHeight * 0.5; + const endThreshold = -windowHeight * 0.4; + + // Only track when section is in the active range + if (sectionTop > startThreshold || sectionTop < endThreshold) { + return; + } + + // Calculate progress through the visible range + const scrollRange = startThreshold - endThreshold; + const scrollProgress = startThreshold - sectionTop; + const progress = Math.max(0, Math.min(1, scrollProgress / scrollRange)); + + // Update scroll progress for visual indicator + setScrollProgress(progress); + + // Feature 1: 30%, Feature 2: 30%, Feature 3: 40% + if (progress < 0.30) { + setActiveFeature(0); + } else if (progress < 0.60) { + setActiveFeature(1); + } else { + setActiveFeature(2); + } + }; + + window.addEventListener('scroll', handleMainScroll, { passive: true }); + handleMainScroll(); + return () => window.removeEventListener('scroll', handleMainScroll); + }, []); + + // Handle scroll-based feature switching for providers section + useEffect(() => { + const handleProviderScroll = () => { + const section = section2Ref.current; + if (!section) return; + + const rect = section.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + const sectionTop = rect.top; + + // Start: section top at 50% viewport (content becoming centered) + // End: section top at -40% viewport (top exiting but bottom still visible) + const startThreshold = windowHeight * 0.5; + const endThreshold = -windowHeight * 0.4; + + if (sectionTop > startThreshold || sectionTop < endThreshold) { + return; + } + + const scrollRange = startThreshold - endThreshold; + const scrollProgress = startThreshold - sectionTop; + const progress = Math.max(0, Math.min(1, scrollProgress / scrollRange)); + + // Update scroll progress for visual indicator + setProviderScrollProgress(progress); + + // Feature 1: 30%, Feature 2: 30%, Feature 3: 40% + if (progress < 0.30) { + setActiveProviderFeature(0); + } else if (progress < 0.60) { + setActiveProviderFeature(1); + } else { + setActiveProviderFeature(2); + } + }; + + window.addEventListener('scroll', handleProviderScroll, { passive: true }); + handleProviderScroll(); + return () => window.removeEventListener('scroll', handleProviderScroll); + }, []); + + // Handle scroll-based feature switching for anywhere section + useEffect(() => { + const handleAnywhereScroll = () => { + const section = section3Ref.current; + if (!section) return; + + const rect = section.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + const sectionTop = rect.top; + + // Start: section top at 50% viewport (content becoming centered) + // End: section top at -40% viewport (top exiting but bottom still visible) + const startThreshold = windowHeight * 0.5; + const endThreshold = -windowHeight * 0.4; + + if (sectionTop > startThreshold || sectionTop < endThreshold) { + return; + } + + const scrollRange = startThreshold - endThreshold; + const scrollProgress = startThreshold - sectionTop; + const progress = Math.max(0, Math.min(1, scrollProgress / scrollRange)); + + // Update scroll progress for visual indicator + setAnywhereScrollProgress(progress); + + // Feature 1: 30%, Feature 2: 30%, Feature 3: 40% + if (progress < 0.30) { + setActiveAnywhereFeature(0); + } else if (progress < 0.60) { + setActiveAnywhereFeature(1); + } else { + setActiveAnywhereFeature(2); + } + }; + + window.addEventListener('scroll', handleAnywhereScroll, { passive: true }); + handleAnywhereScroll(); + return () => window.removeEventListener('scroll', handleAnywhereScroll); + }, []); + + // Handle button clicks + const handleFeatureClick = (featureIndex) => { + setActiveFeature(featureIndex); + }; + + // Handle provider button clicks + const handleProviderFeatureClick = (featureIndex) => { + setActiveProviderFeature(featureIndex); + }; + + // Handle anywhere button clicks + const handleAnywhereFeatureClick = (featureIndex) => { + setActiveAnywhereFeature(featureIndex); + }; + + useEffect(() => { + const navbar = document.querySelector(".navbar"); + const axolotl = document.querySelector(".image-src"); + const controlPlanes = document.querySelectorAll(".flying-cp"); + const clouds = document.querySelectorAll(".cp-cloud-projection"); + + // Trigger animation immediately + axolotl?.classList.add("scrolled"); + controlPlanes.forEach((cp) => cp.classList.add("visible")); + clouds.forEach((cloud) => cloud.classList.add("visible")); + + const handleScroll = () => { + if (window.scrollY < 10) { + navbar?.classList.add("navbar--transparent"); + } else { + navbar?.classList.remove("navbar--transparent"); + } + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + handleScroll(); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + function isInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } + + function FeatureCard({ children }) { + const cardRef = useRef(null); + const glowRef = useRef(null); + + const handleMouseMove = (e) => { + if (!cardRef.current || !glowRef.current) return; + const rect = cardRef.current.getBoundingClientRect(); + glowRef.current.style.left = `${e.clientX - rect.left}px`; + glowRef.current.style.top = `${e.clientY - rect.top}px`; + }; + + return ( +
+
+ {children} +
+ ); + } + + return ( + +
+
+
+

+ open control plane docs + Give your teams the power to run robust, compliant clouds. Public, private, or Sovereign. +

+
+ +
+
+
+
+
+ Crossplane Axolotl + Control Plane + {/* Cloud 1 - Purple/Blue */} + + + + + + + + + + + + + + + + {/* Badge: EU-gov - top left */} + + + EU-gov + + + + + {/* Sonar sweep line */} + + + + + + + + {/* User icon - top left */} + + + + {/* Database icon - top right */} + + + + + {/* Key icon - left */} + + + + + {/* Server icon - right */} + + + + + + {/* Network icon - bottom center */} + + + + + + + + + + + + + Control Plane + {/* Cloud 2-1 - Teal (left) */} + + + + + + + + + + + + + + + + {/* Badge: dev - top left */} + + + dev + + + + + {/* Sonar sweep line */} + + + + + + + + {/* CPU icon - top left */} + + + + + {/* Container/Docker icon - top right */} + + + + + {/* Hard Drive icon - left */} + + + + + {/* Settings icon - right */} + + + + + + + + + + {/* Cloud 2-2 - Pink (right) */} + + + + + + + + + + + + + + + + {/* Badge: prod - top right */} + + + prod + + + + + {/* Sonar sweep line */} + + + + + + + + {/* CPU icon - top left */} + + + + + {/* Container/Docker icon - top right */} + + + + + {/* Hard Drive icon - left */} + + + + + {/* Settings icon - right */} + + + + + + + + + + + Control Plane + {/* Cloud 3 - Orange */} + + + + + + + + + + + + + + + + {/* Badge: EU-public - top right */} + + + EU-public + + + + + {/* Sonar sweep line */} + + + + + + + + {/* CPU icon - top left */} + + + + + {/* User icon - top right */} + + + + {/* Container/Docker icon - left */} + + + + + {/* Memory/RAM icon - right */} + + + + + {/* Globe/Network icon - bottom center */} + + + + + + + + + +
+
+
+
+ +
+
+
+

How it works

+
+ + +

Everything in code

+

Define your entire cloud landscape using code. Always know exactly what's defined and leverage review-based workflows, version control, and much more.

+
+ + +

Continuous self-healing

+

Keep your landscape in sync. Crossplane continuously observes the desired and the actual state and reconciles any differences automatically.

+
+ + +

One syntax for all

+

+ Use a unified approach to define and manage resources across multiple cloud providers and services, reducing infrastructure complexity significantly. +

+
+ + +

Designed for reuse

+

+ Define your landscapes in modular building blocks using Crossplane Compositions or Helm charts. Replicate modules easily across different regions or stages. +

+
+ + +

Run a platform

+

+ Prebuild your own platform tailored to the specific needs of your organization and offer it to development teams in a self-service way. +

+
+ + +

Built for everyone

+

+ Whether you are a cloud expert or just getting started — our providers are designed to help everyone. + We run 100% open-source. +

+
+
+
+
+ +
+ + Start contributing + + + or explore{" "} + + our cloud native ecosystem + + +
+ +
+
+ {/* Full-width title header */} +
+
+

EMPOWER YOUR ENGINEERS

+

Control Planes hold everything they need to orchestrate cloud landscapes

+ {/* Subsection navigation */} +
+ + + +
+
+
+ Read end user guides → + Contribute ServiceProvider → +
+
+ + {/* Centered text content above visual */} +
+
+

Declarative API everywhere

+

Define your infrastructure as declarative YAML. The control plane reconciles your desired state with reality—creating, updating, and managing cloud resources automatically.

+
+
+

Self-healing

+

The control plane continuously monitors your resources and automatically corrects drift, recovers from failures, and ensures your infrastructure matches the desired state at all times—powered by Crossplane.

+
+
+

GitOps

+

Store your control plane configurations in Git. Flux automatically syncs changes from your repository to live environments, providing audit trails, rollback capabilities, and declarative infrastructure management.

+
+
+ + {/* Visual and buttons */} +
+ {/* Visual area */} +
+ {/* Feature 0: Declarative API - use cp2.png */} + Control Plane + + {/* Feature 1: Self-healing - use cp2-crossplane.png */} + Control Plane + + {/* Feature 2: GitOps - use cp2-flux.png */} + Control Plane + + {/* Feature 0: Declarative API - YAML left, cloud right */} +
+
+
+{`apiVersion: openmcp.cloud/v1alpha1
+kind: ManagedControlPlane
+metadata:
+  name: team-prod
+spec:
+  provider: gardener
+---
+apiVersion: openmcp.cloud/v1alpha1
+kind: Database
+metadata:
+  name: prod-db
+spec:
+  engine: postgresql
+---
+apiVersion: openmcp.cloud/v1alpha1
+kind: Subaccount
+metadata:
+  name: team-alpha
+spec:
+  region: eu-central-1`}
+                    
+
+ + {/* Animated line connection - same style as hero */} + + + + + {/* Database and Account icons inside cloud */} +
+ + + + + + + + + + {/* Database icon - left side */} + + + + + + {/* Account/User icon - right side */} + + + + + +
+
+ + {/* Feature 1: Self-healing - 4 resource icons around CP */} +
+
+ {/* Database - top */} +
+
+ + + + +
+ + + +
+ + {/* User - right */} +
+
+ + + + +
+ + + +
+ + {/* Server - bottom */} +
+
+ + + + + + +
+ + + +
+ + {/* Storage - left */} +
+
+ + + + + +
+ + + +
+
+
+ + {/* Feature 2: GitOps - clouds split with badges */} +
+
+ {/* Left cloud */} + + + + + + + + {/* Badge inside SVG */} + + + dev + + + + + + + + + {/* Right cloud */} + + + + + + + + {/* Badge inside SVG */} + + + live + + + + + + + + + {/* Connection lines to CP - same style as hero */} + + + + +
+
+
+
+
+
+ +
+
+ {/* Full-width title header */} +
+
+

READY-TO-USE CONTROL PLANES

+

Provision, manage, secure all instances on open control plane platform

+ {/* Subsection navigation */} +
+ + + +
+
+
+ Read operator guides → + Contribute PlatformProvider → +
+
+ + {/* Centered text content above visual */} +
+
+

Central onboarding API

+

As an operator, provide a central onboarding API where end users can order control planes with standardized configurations. Streamline provisioning and ensure consistency across your organization.

+
+
+

Shared tooling

+

Share common tools across all control planes including Vault for secrets management, Kyverno for policy enforcement, custom IDPs for authentication, and GitHub registries for container images.

+
+
+

Bring own observability stack

+

Integrate your preferred monitoring and observability tools seamlessly with your control planes.

+
+
+ +
+ {/* Visual area */} +
+ {/* Three control planes in triangle formation */} + Control Plane 1 + Control Plane 2 + Control Plane 3 + + {/* Central onboarding API feature - show API icon in center with lines to all CPs */} +
+ + + + + + + + + + + + + + + + +
+ + {/* Shared tooling feature - Vault in center connecting to 3 CPs */} +
+ {/* Central Vault icon */} + + + + + + + + {/* Vault/Storage hexagon icon */} + + + + + + {/* Connection lines from vault to each CP */} + + + + + +
+ + {/* Observability feature - TBD */} +
+ {/* Placeholder for observability visualization */} +
+
+
+
+
+ +
+
+ {/* Full-width title header */} +
+
+

RUNS EVERYWHERE

+

Open Control Plane can be installed wherever Kubernetes is available.

+ {/* Subsection navigation */} +
+ + + +
+
+
+ Read operator guides → + Contribute ClusterProvider → +
+
+ + {/* Centered text content above visual */} +
+
+

Anywhere

+

Deploy control planes anywhere—fully compatible with open source Gardener on any Kubernetes cluster.

+
+
+

Sovereign clouds

+

Meet strict data residency and compliance requirements.

+
+
+

Kind New

+

Develop and test locally using Kind clusters.

+
+
+ +
+ {/* Visual area */} +
+ + {/* Feature 0: Anywhere - gardener with cloud provider badges */} +
+ Gardener + + {/* Flying control planes - reduced to 2 */} + Flying CP + Flying CP + + {/* Cloud provider badges */} +
AWS
+
Azure
+
GCP
+
...
+ + {/* Connection lines from hangar to badges - tree structure */} + + + + + + +
+ + {/* Feature 1: Sovereign clouds - gardener with EU badges */} +
+ Gardener + + {/* Flying control planes - reduced to 2 */} + Flying CP + Flying CP + + {/* Sovereign cloud badges */} +
EU-pub-dev
+
EU-pub-live
+
EU-gov-live
+ + {/* Connection lines from hangar to badges - tree structure */} + + + + + +
+ + {/* Feature 2: Kind */} +
+ Kind + + {/* Kind dev mode - 2 control planes that stay and move around */} + Kind CP 1 + Kind CP 2 +
+
+
+
+
+ +
+
+
+
+
100% open-source
+
+

+ This project started as an inner-source initiative within SAP. Thanks to NeonEphos, it now operates 100% publicly and has been donated to the OpenControlPlane organization. +
+
+ + We believe that no single team can achieve a fully orchestrated environment on their own. +
Only through collaboration we can make Europe's cloud services 100% orchestratable for everyone. +
+
+
+

+ + 💪 See our projects + +
+ + and learn how to contribute + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/theme/Footer/index.js b/src/theme/Footer/index.js new file mode 100644 index 0000000..7e611ec --- /dev/null +++ b/src/theme/Footer/index.js @@ -0,0 +1,71 @@ +/* eslint-disable import/no-unresolved */ +import React from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Link from '@docusaurus/Link'; + +export default function Footer() { + const bmwkEuImg = useBaseUrl('/img/BMWK-EU.png'); + + return ( +
+ {/* Row 1: EU Funding Banner */} +
+
+
+ EU and BMWK funding logos +
+
+

+ Funded by the European Union – NextGenerationEU. +

+

+ The views and opinions expressed are solely those of the author(s) and do not + necessarily reflect the views of the European Union or the European Commission. + Neither the European Union nor the European Commission can be held responsible + for them. +

+
+
+ + NeoNephos Logo + +
+
+
+ + {/* Row 2: Copyright */} +
+
+

+ Copyright © Linux Foundation Europe.{' '} + Open Control Plane is a project of the Open Component Model Community. For + applicable policies including privacy policy, terms of use and trademark usage + guidelines, please see{' '} + + https://linuxfoundation.eu + + . Linux is a registered trademark of Linus Torvalds. +

+
+
+ + {/* Row 3: Legal Links */} +
+
+ +
+
+
+ ); +} diff --git a/src/theme/Root/index.js b/src/theme/Root/index.js new file mode 100644 index 0000000..2dba523 --- /dev/null +++ b/src/theme/Root/index.js @@ -0,0 +1,59 @@ +import React, { useEffect } from 'react'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +export default function Root({ children }) { + useEffect(() => { + if (!ExecutionEnvironment.canUseDOM) { + return; + } + + // Auto-collapse other sidebar categories when one is expanded + const handleCategoryClick = () => { + const sidebar = document.querySelector('.theme-doc-sidebar-menu'); + if (!sidebar) return; + + const categories = sidebar.querySelectorAll('.theme-doc-sidebar-item-category'); + + categories.forEach(category => { + const button = category.querySelector('.menu__link--sublist'); + if (!button) return; + + const originalClick = button.onclick; + + button.onclick = function(e) { + // Get all other categories + categories.forEach(otherCategory => { + if (otherCategory !== category) { + const otherButton = otherCategory.querySelector('.menu__link--sublist'); + const otherList = otherCategory.querySelector('.menu__list'); + + if (otherList && !otherList.classList.contains('menu__list--collapsed')) { + // Close the other category + otherButton?.click(); + } + } + }); + + // Let the original handler run + if (originalClick) { + originalClick.call(this, e); + } + }; + }); + }; + + // Run on mount and after navigation + handleCategoryClick(); + + // Re-run when sidebar renders + const observer = new MutationObserver(handleCategoryClick); + const sidebar = document.querySelector('.theme-doc-sidebar-menu'); + if (sidebar) { + observer.observe(sidebar, { childList: true, subtree: true }); + } + + return () => observer.disconnect(); + }, []); + + return <>{children}; +} diff --git a/static/img/co_axolotl.png b/static/img/co_axolotl.png new file mode 100644 index 0000000..66a9ea7 Binary files /dev/null and b/static/img/co_axolotl.png differ diff --git a/static/img/co_axolotl.svg b/static/img/co_axolotl.svg new file mode 100644 index 0000000..0e25fe6 --- /dev/null +++ b/static/img/co_axolotl.svg @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/img/co_axolotl_mirrored.png b/static/img/co_axolotl_mirrored.png new file mode 100644 index 0000000..cc5e0c4 Binary files /dev/null and b/static/img/co_axolotl_mirrored.png differ diff --git a/static/img/co_axolotl_transparent.png b/static/img/co_axolotl_transparent.png new file mode 100644 index 0000000..b1270c8 Binary files /dev/null and b/static/img/co_axolotl_transparent.png differ diff --git a/static/img/contribution/Picture.png b/static/img/contribution/Picture.png new file mode 100644 index 0000000..b361d21 Binary files /dev/null and b/static/img/contribution/Picture.png differ diff --git a/static/img/contribution/Picture0.png b/static/img/contribution/Picture0.png new file mode 100644 index 0000000..ccc2655 Binary files /dev/null and b/static/img/contribution/Picture0.png differ diff --git a/static/img/contribution/Picture1.png b/static/img/contribution/Picture1.png new file mode 100644 index 0000000..8d002c0 Binary files /dev/null and b/static/img/contribution/Picture1.png differ diff --git a/static/img/contribution/contribution_open_github_actions.png b/static/img/contribution/contribution_open_github_actions.png new file mode 100644 index 0000000..1f3e4ac Binary files /dev/null and b/static/img/contribution/contribution_open_github_actions.png differ diff --git a/static/img/contribution/contribution_open_publish_release_candidate.png b/static/img/contribution/contribution_open_publish_release_candidate.png new file mode 100644 index 0000000..f8c2245 Binary files /dev/null and b/static/img/contribution/contribution_open_publish_release_candidate.png differ diff --git a/static/img/contribution/contribution_search_for_branch.png b/static/img/contribution/contribution_search_for_branch.png new file mode 100644 index 0000000..044cef6 Binary files /dev/null and b/static/img/contribution/contribution_search_for_branch.png differ diff --git a/static/img/contribution/contribution_select_package.png b/static/img/contribution/contribution_select_package.png new file mode 100644 index 0000000..45592e6 Binary files /dev/null and b/static/img/contribution/contribution_select_package.png differ diff --git a/static/img/contribution/contribution_select_your_image.png b/static/img/contribution/contribution_select_your_image.png new file mode 100644 index 0000000..9311a56 Binary files /dev/null and b/static/img/contribution/contribution_select_your_image.png differ diff --git a/static/img/contribution/create_issue.gif b/static/img/contribution/create_issue.gif new file mode 100644 index 0000000..bff2153 Binary files /dev/null and b/static/img/contribution/create_issue.gif differ diff --git a/static/img/contribution/github_discussion.png b/static/img/contribution/github_discussion.png new file mode 100644 index 0000000..91220e0 Binary files /dev/null and b/static/img/contribution/github_discussion.png differ diff --git a/static/img/cp1.png b/static/img/cp1.png new file mode 100644 index 0000000..c3d1fb3 Binary files /dev/null and b/static/img/cp1.png differ diff --git a/static/img/cp2.png b/static/img/cp2.png new file mode 100644 index 0000000..881cb62 Binary files /dev/null and b/static/img/cp2.png differ diff --git a/static/img/cp3.png b/static/img/cp3.png new file mode 100644 index 0000000..0cac594 Binary files /dev/null and b/static/img/cp3.png differ diff --git a/static/img/cp4.png b/static/img/cp4.png new file mode 100644 index 0000000..e675875 Binary files /dev/null and b/static/img/cp4.png differ diff --git a/static/img/docusaurus-social-card.jpg b/static/img/docusaurus-social-card.jpg deleted file mode 100644 index ffcb448..0000000 Binary files a/static/img/docusaurus-social-card.jpg and /dev/null differ diff --git a/static/img/docusaurus.png b/static/img/docusaurus.png deleted file mode 100644 index f458149..0000000 Binary files a/static/img/docusaurus.png and /dev/null differ diff --git a/static/img/favicon.ico b/static/img/favicon.ico index c01d54b..56bf504 100644 Binary files a/static/img/favicon.ico and b/static/img/favicon.ico differ diff --git a/static/img/icons/icon-align-dark.png b/static/img/icons/icon-align-dark.png new file mode 100644 index 0000000..fd13242 Binary files /dev/null and b/static/img/icons/icon-align-dark.png differ diff --git a/static/img/icons/icon-align.png b/static/img/icons/icon-align.png new file mode 100644 index 0000000..6202a8e Binary files /dev/null and b/static/img/icons/icon-align.png differ diff --git a/static/img/icons/icon-code-dark.png b/static/img/icons/icon-code-dark.png new file mode 100644 index 0000000..f3e0d5b Binary files /dev/null and b/static/img/icons/icon-code-dark.png differ diff --git a/static/img/icons/icon-code.png b/static/img/icons/icon-code.png new file mode 100644 index 0000000..fff2348 Binary files /dev/null and b/static/img/icons/icon-code.png differ diff --git a/static/img/icons/icon-platform-dark.png b/static/img/icons/icon-platform-dark.png new file mode 100644 index 0000000..a283bd2 Binary files /dev/null and b/static/img/icons/icon-platform-dark.png differ diff --git a/static/img/icons/icon-platform.png b/static/img/icons/icon-platform.png new file mode 100644 index 0000000..bb43e1c Binary files /dev/null and b/static/img/icons/icon-platform.png differ diff --git a/static/img/icons/icon-puzzle-dark.png b/static/img/icons/icon-puzzle-dark.png new file mode 100644 index 0000000..9ea7531 Binary files /dev/null and b/static/img/icons/icon-puzzle-dark.png differ diff --git a/static/img/icons/icon-puzzle.png b/static/img/icons/icon-puzzle.png new file mode 100644 index 0000000..4e6c16d Binary files /dev/null and b/static/img/icons/icon-puzzle.png differ diff --git a/static/img/icons/icon-reconcile-dark.png b/static/img/icons/icon-reconcile-dark.png new file mode 100644 index 0000000..f8f57d9 Binary files /dev/null and b/static/img/icons/icon-reconcile-dark.png differ diff --git a/static/img/icons/icon-reconcile.png b/static/img/icons/icon-reconcile.png new file mode 100644 index 0000000..1d6e440 Binary files /dev/null and b/static/img/icons/icon-reconcile.png differ diff --git a/static/img/icons/icon-simple-dark.png b/static/img/icons/icon-simple-dark.png new file mode 100644 index 0000000..9a2212b Binary files /dev/null and b/static/img/icons/icon-simple-dark.png differ diff --git a/static/img/icons/icon-simple.png b/static/img/icons/icon-simple.png new file mode 100644 index 0000000..88fddba Binary files /dev/null and b/static/img/icons/icon-simple.png differ diff --git a/static/img/landingpage-crossplane.png b/static/img/landingpage-crossplane.png new file mode 100644 index 0000000..0a09821 Binary files /dev/null and b/static/img/landingpage-crossplane.png differ diff --git a/static/img/logo.svg b/static/img/logo.svg deleted file mode 100644 index 9db6d0d..0000000 --- a/static/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/img/logos/crossplane.png b/static/img/logos/crossplane.png new file mode 100644 index 0000000..94280b8 Binary files /dev/null and b/static/img/logos/crossplane.png differ diff --git a/static/img/logos/external-secrets.png b/static/img/logos/external-secrets.png new file mode 100644 index 0000000..49d1907 Binary files /dev/null and b/static/img/logos/external-secrets.png differ diff --git a/static/img/logos/flux.png b/static/img/logos/flux.png new file mode 100644 index 0000000..1efd6f8 Binary files /dev/null and b/static/img/logos/flux.png differ diff --git a/static/img/logos/gardener.png b/static/img/logos/gardener.png new file mode 100644 index 0000000..a845665 Binary files /dev/null and b/static/img/logos/gardener.png differ diff --git a/static/img/logos/kubernetes.png b/static/img/logos/kubernetes.png new file mode 100644 index 0000000..1a9f546 Binary files /dev/null and b/static/img/logos/kubernetes.png differ diff --git a/static/img/logos/kyverno.png b/static/img/logos/kyverno.png new file mode 100644 index 0000000..4e33936 Binary files /dev/null and b/static/img/logos/kyverno.png differ diff --git a/static/img/logos/landscaper.png b/static/img/logos/landscaper.png new file mode 100644 index 0000000..a845665 Binary files /dev/null and b/static/img/logos/landscaper.png differ diff --git a/static/img/logos/ocm.png b/static/img/logos/ocm.png new file mode 100644 index 0000000..88c7d30 Binary files /dev/null and b/static/img/logos/ocm.png differ diff --git a/static/img/logos/ocm.svg b/static/img/logos/ocm.svg new file mode 100644 index 0000000..f810b76 --- /dev/null +++ b/static/img/logos/ocm.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/img/neonephos.svg b/static/img/neonephos.svg new file mode 100644 index 0000000..3dd93e7 --- /dev/null +++ b/static/img/neonephos.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/static/img/platform/Group 6092.png b/static/img/platform/Group 6092.png new file mode 100644 index 0000000..984d6a7 Binary files /dev/null and b/static/img/platform/Group 6092.png differ diff --git a/static/img/platform/antenna_btm.png b/static/img/platform/antenna_btm.png new file mode 100644 index 0000000..6ff96e7 Binary files /dev/null and b/static/img/platform/antenna_btm.png differ diff --git a/static/img/platform/cp2-crossplane.png b/static/img/platform/cp2-crossplane.png new file mode 100644 index 0000000..dc71e90 Binary files /dev/null and b/static/img/platform/cp2-crossplane.png differ diff --git a/static/img/platform/cp2-flux.png b/static/img/platform/cp2-flux.png new file mode 100644 index 0000000..985f817 Binary files /dev/null and b/static/img/platform/cp2-flux.png differ diff --git a/static/img/platform/hangar_gardener.png b/static/img/platform/hangar_gardener.png new file mode 100644 index 0000000..5c53ae9 Binary files /dev/null and b/static/img/platform/hangar_gardener.png differ diff --git a/static/img/platform/hangar_kind.png b/static/img/platform/hangar_kind.png new file mode 100644 index 0000000..dcd55e7 Binary files /dev/null and b/static/img/platform/hangar_kind.png differ diff --git a/static/img/platform/main.png b/static/img/platform/main.png new file mode 100644 index 0000000..053e4a8 Binary files /dev/null and b/static/img/platform/main.png differ diff --git a/static/img/platform/platform_left.png b/static/img/platform/platform_left.png new file mode 100644 index 0000000..7110b4d Binary files /dev/null and b/static/img/platform/platform_left.png differ diff --git a/static/img/platform/platform_right.png b/static/img/platform/platform_right.png new file mode 100644 index 0000000..88dd9fa Binary files /dev/null and b/static/img/platform/platform_right.png differ diff --git a/static/img/platform/radar_left.png b/static/img/platform/radar_left.png new file mode 100644 index 0000000..09658fa Binary files /dev/null and b/static/img/platform/radar_left.png differ diff --git a/static/img/platform/radar_right.png b/static/img/platform/radar_right.png new file mode 100644 index 0000000..20b339e Binary files /dev/null and b/static/img/platform/radar_right.png differ diff --git a/static/img/platform/tower.png b/static/img/platform/tower.png new file mode 100644 index 0000000..99d2266 Binary files /dev/null and b/static/img/platform/tower.png differ diff --git a/static/img/platform/tower_crossplane.png b/static/img/platform/tower_crossplane.png new file mode 100644 index 0000000..fccd903 Binary files /dev/null and b/static/img/platform/tower_crossplane.png differ diff --git a/static/img/platform/tower_landscaper.png b/static/img/platform/tower_landscaper.png new file mode 100644 index 0000000..c4c4ccf Binary files /dev/null and b/static/img/platform/tower_landscaper.png differ diff --git a/static/img/platform/tower_velero.png b/static/img/platform/tower_velero.png new file mode 100644 index 0000000..9240fbf Binary files /dev/null and b/static/img/platform/tower_velero.png differ diff --git a/static/img/provider_types.png b/static/img/provider_types.png new file mode 100644 index 0000000..7284bfe Binary files /dev/null and b/static/img/provider_types.png differ diff --git a/static/img/undraw_docusaurus_mountain.svg b/static/img/undraw_docusaurus_mountain.svg deleted file mode 100644 index af961c4..0000000 --- a/static/img/undraw_docusaurus_mountain.svg +++ /dev/null @@ -1,171 +0,0 @@ - - Easy to Use - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/img/undraw_docusaurus_react.svg b/static/img/undraw_docusaurus_react.svg deleted file mode 100644 index 94b5cf0..0000000 --- a/static/img/undraw_docusaurus_react.svg +++ /dev/null @@ -1,170 +0,0 @@ - - Powered by React - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/img/undraw_docusaurus_tree.svg b/static/img/undraw_docusaurus_tree.svg deleted file mode 100644 index d9161d3..0000000 --- a/static/img/undraw_docusaurus_tree.svg +++ /dev/null @@ -1,40 +0,0 @@ - - Focus on What Matters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -