mirror of
https://github.com/netbirdio/netbird-crossplane-provider.git
synced 2026-05-22 18:44:14 -07:00
341 lines
12 KiB
Go
341 lines
12 KiB
Go
/*
|
|
Copyright 2022 The Crossplane Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package nbnetwork
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
|
"github.com/crossplane/crossplane-runtime/pkg/connection"
|
|
"github.com/crossplane/crossplane-runtime/pkg/controller"
|
|
"github.com/crossplane/crossplane-runtime/pkg/event"
|
|
"github.com/crossplane/crossplane-runtime/pkg/feature"
|
|
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
"github.com/crossplane/crossplane-runtime/pkg/ratelimiter"
|
|
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
|
|
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
apisv1alpha1 "github.com/crossplane/netbird-crossplane-provider/apis/v1alpha1"
|
|
"github.com/crossplane/netbird-crossplane-provider/apis/vpn/v1alpha1"
|
|
auth "github.com/crossplane/netbird-crossplane-provider/internal/controller/nb"
|
|
"github.com/crossplane/netbird-crossplane-provider/internal/features"
|
|
"github.com/go-logr/logr"
|
|
"github.com/google/go-cmp/cmp"
|
|
netbird "github.com/netbirdio/netbird/management/client/rest"
|
|
nbapi "github.com/netbirdio/netbird/management/server/http/api"
|
|
"github.com/pkg/errors"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
)
|
|
|
|
const (
|
|
errNotNbNetwork = "managed resource is not a NbNetwork custom resource"
|
|
errTrackPCUsage = "cannot track ProviderConfig usage"
|
|
errGetPC = "cannot get ProviderConfig"
|
|
errGetCreds = "cannot get credentials"
|
|
)
|
|
|
|
// Setup adds a controller that reconciles NbNetwork managed resources.
|
|
func Setup(mgr ctrl.Manager, o controller.Options) error {
|
|
name := managed.ControllerName(v1alpha1.NbNetworkGroupKind)
|
|
|
|
cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())}
|
|
if o.Features.Enabled(features.EnableAlphaExternalSecretStores) {
|
|
cps = append(cps, connection.NewDetailsManager(mgr.GetClient(), apisv1alpha1.StoreConfigGroupVersionKind))
|
|
}
|
|
|
|
reconcilerOptions := []managed.ReconcilerOption{
|
|
managed.WithExternalConnecter(&connector{
|
|
SharedConnector: auth.NewSharedConnector(
|
|
mgr.GetClient(),
|
|
resource.NewProviderConfigUsageTracker(mgr.GetClient(), &apisv1alpha1.ProviderConfigUsage{}),
|
|
),
|
|
}),
|
|
managed.WithLogger(o.Logger.WithValues("controller", name)),
|
|
managed.WithPollInterval(o.PollInterval),
|
|
managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),
|
|
managed.WithConnectionPublishers(cps...),
|
|
managed.WithInitializers(),
|
|
}
|
|
if o.Features.Enabled(feature.EnableBetaManagementPolicies) {
|
|
reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies())
|
|
}
|
|
|
|
r := managed.NewReconciler(mgr,
|
|
resource.ManagedKind(v1alpha1.NbNetworkGroupVersionKind),
|
|
reconcilerOptions...,
|
|
)
|
|
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
Named(name).
|
|
WithOptions(o.ForControllerRuntime()).
|
|
WithEventFilter(resource.DesiredStateChanged()).
|
|
For(&v1alpha1.NbNetwork{}).
|
|
Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter))
|
|
}
|
|
|
|
// A connector is expected to produce an ExternalClient when its Connect method
|
|
// is called.
|
|
type connector struct {
|
|
*auth.SharedConnector
|
|
}
|
|
|
|
// Connect typically produces an ExternalClient by:
|
|
// 1. Tracking that the managed resource is using a ProviderConfig.
|
|
// 2. Getting the managed resource's ProviderConfig.
|
|
// 3. Getting the credentials specified by the ProviderConfig.
|
|
// 4. Using the credentials to form a client.
|
|
func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) {
|
|
_, ok := mg.(*v1alpha1.NbNetwork)
|
|
if !ok {
|
|
return nil, errors.New(errNotNbNetwork)
|
|
}
|
|
|
|
pc, err := c.SharedConnector.GetProviderConfig(ctx, mg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authManager, err := c.SharedConnector.Connect(ctx, mg, pc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &external{
|
|
authManager: authManager,
|
|
log: ctrl.Log.WithName("provider-nbnetwork"),
|
|
}, nil
|
|
}
|
|
|
|
// An ExternalClient observes, then either creates, updates, or deletes an
|
|
// external resource to ensure it reflects the managed resource's desired state.
|
|
type authClient interface {
|
|
GetClient(ctx context.Context) (*netbird.Client, error)
|
|
ForceRefresh(ctx context.Context) error
|
|
}
|
|
|
|
// external implements managed.ExternalClient for the NbNetwork managed resource.
|
|
type external struct {
|
|
authManager authClient
|
|
log logr.Logger
|
|
}
|
|
|
|
// Observe checks whether the NbNetwork currently exists in netbird and updates status.
|
|
func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) {
|
|
cr, ok := mg.(*v1alpha1.NbNetwork)
|
|
if !ok {
|
|
return managed.ExternalObservation{}, errors.New(errNotNbNetwork)
|
|
}
|
|
client, err := c.authManager.GetClient(ctx)
|
|
if err != nil {
|
|
return managed.ExternalObservation{}, errors.Wrap(err, "failed to get authenticated client")
|
|
}
|
|
c.log.Info("observing", "cr", cr)
|
|
externalName := meta.GetExternalName(cr)
|
|
lookupID := resolveNetworkLookupID(cr)
|
|
|
|
// Adoption pattern: if we don't yet have a stable provider ID, try to find by Name.
|
|
// lookupID == "" covers a fresh resource with no external name and no status ID.
|
|
// lookupID == cr.Name covers older reconciles that defaulted external-name to the k8s
|
|
// object name without ever recording a real status ID.
|
|
if lookupID == "" || lookupID == cr.Name {
|
|
networks, err := client.Networks.List(ctx)
|
|
if err != nil {
|
|
if auth.IsTokenInvalidError(err) {
|
|
c.authManager.ForceRefresh(ctx)
|
|
return managed.ExternalObservation{}, err
|
|
}
|
|
c.log.Info("failed to list networks")
|
|
return managed.ExternalObservation{
|
|
ResourceExists: false,
|
|
}, nil //return nil so that observe can return without error so that it passes to create.
|
|
}
|
|
for _, net := range networks {
|
|
if net.Name == cr.Spec.ForProvider.Name {
|
|
meta.SetExternalName(cr, net.Id)
|
|
cr.Status.AtProvider = v1alpha1.NbNetworkObservation{
|
|
Id: net.Id,
|
|
Resources: &net.Resources,
|
|
Description: net.Description,
|
|
Name: net.Name,
|
|
Policies: &net.Policies,
|
|
Routers: &net.Routers,
|
|
RoutingPeersCount: net.RoutingPeersCount,
|
|
}
|
|
cr.Status.SetConditions(xpv1.Available())
|
|
return managed.ExternalObservation{
|
|
ResourceExists: true,
|
|
ResourceUpToDate: false, // force requeue to persist external name
|
|
}, nil
|
|
}
|
|
}
|
|
// Not found by name, treat as not existing
|
|
return managed.ExternalObservation{ResourceExists: false}, nil
|
|
}
|
|
|
|
// If we have a resolved lookup ID, fetch by ID.
|
|
network, err := client.Networks.Get(ctx, lookupID)
|
|
if err != nil {
|
|
if auth.IsTokenInvalidError(err) {
|
|
c.authManager.ForceRefresh(ctx)
|
|
return managed.ExternalObservation{}, err
|
|
}
|
|
if isNetworkNotFoundError(err) {
|
|
c.log.Info("network not found", "lookup-id", lookupID)
|
|
return managed.ExternalObservation{
|
|
ResourceExists: false,
|
|
}, nil
|
|
}
|
|
// Don't swallow transient errors — Crossplane should requeue, not call Create.
|
|
return managed.ExternalObservation{}, errors.Wrapf(err, "failed to observe network %q", lookupID)
|
|
}
|
|
|
|
// Repair stale or missing external-name annotations after recovering via status ID.
|
|
if externalName != network.Id {
|
|
meta.SetExternalName(cr, network.Id)
|
|
}
|
|
|
|
cr.Status.AtProvider = v1alpha1.NbNetworkObservation{
|
|
Id: network.Id,
|
|
Resources: &network.Resources,
|
|
Description: network.Description,
|
|
Name: network.Name,
|
|
Policies: &network.Policies,
|
|
Routers: &network.Routers,
|
|
RoutingPeersCount: network.RoutingPeersCount,
|
|
}
|
|
|
|
cr.Status.SetConditions(xpv1.Available())
|
|
|
|
return managed.ExternalObservation{
|
|
ResourceExists: true,
|
|
ResourceUpToDate: isnetworkuptodate(network, cr.Spec.ForProvider),
|
|
}, nil
|
|
}
|
|
|
|
// resolveNetworkLookupID picks the best identifier to use when looking the network
|
|
// up by ID, falling back to the recorded provider ID when the external-name
|
|
// annotation is missing or was defaulted to the Kubernetes object name by an
|
|
// older reconcile (before WithInitializers disabled the NameAsExternalName default).
|
|
func resolveNetworkLookupID(cr *v1alpha1.NbNetwork) string {
|
|
externalName := meta.GetExternalName(cr)
|
|
switch {
|
|
case externalName == "":
|
|
return cr.Status.AtProvider.Id
|
|
case cr.Status.AtProvider.Id != "" && externalName == cr.GetName() && cr.Status.AtProvider.Id != externalName:
|
|
// Recover from older reconciles that defaulted the external name to the Kubernetes object name.
|
|
return cr.Status.AtProvider.Id
|
|
default:
|
|
return externalName
|
|
}
|
|
}
|
|
|
|
// isNetworkNotFoundError matches the "network: <id> not found" message
|
|
// returned by the netbird REST API for a missing network.
|
|
func isNetworkNotFoundError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
errStr := strings.ToLower(err.Error())
|
|
return strings.Contains(errStr, "network") && strings.Contains(errStr, "not found")
|
|
}
|
|
|
|
// isnetworkuptodate reports whether the netbird Network matches the desired spec.
|
|
func isnetworkuptodate(network *nbapi.Network, nbNetworkParameters v1alpha1.NbNetworkParameters) bool {
|
|
if !cmp.Equal(*network.Description, nbNetworkParameters.Description) {
|
|
return false
|
|
}
|
|
if !cmp.Equal(network.Name, nbNetworkParameters.Name) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Create provisions a new netbird Network for the managed resource.
|
|
func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) {
|
|
cr, ok := mg.(*v1alpha1.NbNetwork)
|
|
if !ok {
|
|
return managed.ExternalCreation{}, errors.New(errNotNbNetwork)
|
|
}
|
|
client, err := c.authManager.GetClient(ctx)
|
|
if err != nil {
|
|
return managed.ExternalCreation{}, errors.Wrap(err, "failed to get authenticated client")
|
|
}
|
|
c.log.Info("creating", "cr", cr)
|
|
network, err := client.Networks.Create(ctx, nbapi.NetworkRequest{
|
|
Name: cr.Spec.ForProvider.Name,
|
|
Description: &cr.Spec.ForProvider.Description,
|
|
})
|
|
|
|
if err != nil {
|
|
return managed.ExternalCreation{
|
|
// Optionally return any details that may be required to connect to the
|
|
// external resource. These will be stored as the connection secret.
|
|
ConnectionDetails: managed.ConnectionDetails{},
|
|
}, err
|
|
}
|
|
meta.SetExternalName(cr, network.Id)
|
|
return managed.ExternalCreation{}, nil
|
|
}
|
|
|
|
// Update applies the desired spec to the existing netbird Network.
|
|
func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) {
|
|
cr, ok := mg.(*v1alpha1.NbNetwork)
|
|
if !ok {
|
|
return managed.ExternalUpdate{}, errors.New(errNotNbNetwork)
|
|
}
|
|
client, err := c.authManager.GetClient(ctx)
|
|
if err != nil {
|
|
return managed.ExternalUpdate{}, errors.Wrap(err, "failed to get authenticated client")
|
|
}
|
|
networkid := resolveNetworkLookupID(cr)
|
|
if networkid == "" {
|
|
return managed.ExternalUpdate{}, errors.New("can't find network id")
|
|
}
|
|
c.log.Info("Updating", "cr", cr)
|
|
_, err = client.Networks.Update(ctx, networkid, nbapi.PutApiNetworksNetworkIdJSONRequestBody{
|
|
Name: cr.Spec.ForProvider.Name,
|
|
Description: &cr.Spec.ForProvider.Description,
|
|
})
|
|
if err != nil {
|
|
return managed.ExternalUpdate{}, err
|
|
}
|
|
|
|
return managed.ExternalUpdate{
|
|
// Optionally return any details that may be required to connect to the
|
|
// external resource. These will be stored as the connection secret.
|
|
ConnectionDetails: managed.ConnectionDetails{},
|
|
}, nil
|
|
}
|
|
|
|
// Delete removes the netbird Network associated with this managed resource.
|
|
func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
|
|
cr, ok := mg.(*v1alpha1.NbNetwork)
|
|
if !ok {
|
|
return errors.New(errNotNbNetwork)
|
|
}
|
|
client, err := c.authManager.GetClient(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get authenticated client")
|
|
}
|
|
c.log.Info("Deleting", "cr", cr)
|
|
networkid := resolveNetworkLookupID(cr)
|
|
if networkid == "" {
|
|
return errors.New("can't find network id")
|
|
}
|
|
return client.Networks.Delete(ctx, networkid)
|
|
}
|