Working client and Realm controller

This commit is contained in:
paul 2021-01-11 06:32:28 +01:00
parent fc6f1632da
commit f949a523be
6 changed files with 339 additions and 27 deletions

View file

@ -20,33 +20,29 @@ type Keycloak struct {
user, pass, realm string user, pass, realm string
} }
func (kc *Keycloak) CreateRealmIfNotExists(ctx context.Context, realm gocloak.RealmRepresentation) error { func (kc *Keycloak) CreateRealm(ctx context.Context, realm gocloak.RealmRepresentation) error {
_, err := kc.client.CreateRealm(ctx, kc.getToken(), realm) _, err := kc.client.CreateRealm(ctx, kc.getToken(), realm)
if isConflict(err) {
log.Printf("Realm '%s' already exists, not updated", *realm.Realm)
return nil
}
return err return err
} }
func (kc *Keycloak) CreateProviderIfNotExists(ctx context.Context, realm string, provider gocloak.IdentityProviderRepresentation) error { func (kc *Keycloak) UpdateRealm(ctx context.Context, realm gocloak.RealmRepresentation) error {
return kc.client.UpdateRealm(ctx, kc.getToken(), realm)
}
func (kc *Keycloak) CreateProvider(ctx context.Context, realm string, provider gocloak.IdentityProviderRepresentation) error {
_, err := kc.client.CreateIdentityProvider(ctx, kc.getToken(), realm, provider) _, err := kc.client.CreateIdentityProvider(ctx, kc.getToken(), realm, provider)
if isConflict(err) {
log.Printf("Provider '%s/%s' already exists, not updated", realm, *provider.Alias)
return nil
}
return err return err
} }
func (kc *Keycloak) CreateClientIfNotExists(ctx context.Context, realm string, c gocloak.Client) error { func (kc *Keycloak) CreateClient(ctx context.Context, realm string, c gocloak.Client) error {
_, err := kc.client.CreateClient(ctx, kc.getToken(), realm, c) _, err := kc.client.CreateClient(ctx, kc.getToken(), realm, c)
if isConflict(err) {
log.Printf("Client '%s/%s' already exists, not updated", realm, *c.ClientID)
return nil
}
return err return err
} }
func (kc *Keycloak) UpdateClient(ctx context.Context, realm string, c gocloak.Client) error {
return kc.client.UpdateClient(ctx, kc.getToken(), realm, c)
}
func New(url, user, pass, realm string) (*Keycloak, error) { func New(url, user, pass, realm string) (*Keycloak, error) {
kc := &Keycloak{} kc := &Keycloak{}
kc.client = gocloak.NewClient(url) kc.client = gocloak.NewClient(url)

View file

@ -0,0 +1,62 @@
package controllers
import (
keycloakv1alpha1 "git.zom.bi/images/keycloak-operator/api/v1alpha1"
"github.com/Nerzal/gocloak/v7"
)
// ConvertToClient takes a CRD representation and converts it into a datatype
// that can be understood by GoCloak.
func ConvertToClient(clientCrd keycloakv1alpha1.KeycloakClient) (gocloak.Client, error) {
var client gocloak.Client
clientSpec := clientCrd.Spec
// Mandatory Properties
client.ClientID = &clientSpec.ClientID
// Optional Properties
client.Enabled = clientSpec.Enabled
client.Name = clientSpec.Name
client.Description = clientSpec.Description
client.Protocol = clientSpec.Protocol
client.ClientAuthenticatorType = clientSpec.ClientAuthenticatorType
client.DirectAccessGrantsEnabled = clientSpec.DirectAccessGrantsEnabled
client.PublicClient = clientSpec.PublicClient
client.ImplicitFlowEnabled = clientSpec.ImplicitFlowEnabled
client.StandardFlowEnabled = clientSpec.StandardFlowEnabled
client.ServiceAccountsEnabled = clientSpec.ServiceAccountsEnabled
client.RegistrationAccessToken = clientSpec.RegistrationAccessToken
client.SurrogateAuthRequired = clientSpec.SurrogateAuthRequired
client.BearerOnly = clientSpec.BearerOnly
client.ConsentRequired = clientSpec.ConsentRequired
client.DefaultClientScopes = clientSpec.DefaultClientScopes
client.OptionalClientScopes = clientSpec.OptionalClientScopes
client.BaseURL = clientSpec.BaseURL
client.RootURL = clientSpec.RootURL
client.AdminURL = clientSpec.AdminURL
client.RedirectURIs = clientSpec.RedirectURIs
client.WebOrigins = clientSpec.WebOrigins
if clientSpec.Secret != nil {
// TODO
// client.Secret = ""
}
// client.Access = ""
// client.Attributes = ""
// client.AuthenticationFlowBindingOverrides = ""
// client.AuthorizationServicesEnabled = ""
// client.AuthorizationSettings = ""
// client.DefaultRoles = ""
// client.FrontChannelLogout = ""
// client.FullScopeAllowed = ""
// client.ID = ""
// client.NodeReRegistrationTimeout = ""
// client.NotBefore = ""
// client.Origin = ""
// client.ProtocolMappers = ""
// client.RegisteredNodes = ""
return client, nil
}

View file

@ -19,7 +19,9 @@ package controllers
import ( import (
"context" "context"
"github.com/Nerzal/gocloak/v7"
"github.com/go-logr/logr" "github.com/go-logr/logr"
apierrs "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@ -50,9 +52,79 @@ type KeycloakClientReconciler struct {
// For more details, check Reconcile and its Result here: // For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
func (r *KeycloakClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { func (r *KeycloakClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = r.Log.WithValues("keycloakclient", req.NamespacedName) log := r.Log.WithValues("keycloakclient", req.NamespacedName)
// your logic here log.Info("reconciling")
// We get the information from the CRD
var client keycloakv1alpha1.KeycloakClient
if err := r.Get(ctx, req.NamespacedName, &client); err != nil {
if apierrs.IsNotFound(err) {
// Client is already deleted via finalizer.
return ctrl.Result{}, nil
}
}
if !client.ObjectMeta.DeletionTimestamp.IsZero() {
// is in the process of being deleted
if containsString(client.ObjectMeta.Finalizers, FinalizerName) {
// our finalizer is present, so lets handle any external dependency
// We do not want to delete anything, so we just disable the client.
disabled := gocloak.Client{ClientID: &client.Spec.ClientID, Enabled: gocloak.BoolP(false)}
err := r.Keycloak.UpdateClient(ctx, client.Spec.RealmName, disabled)
if err != nil {
// if fail to delete the external dependency here, return with error
// so that it can be retried
return ctrl.Result{}, err
}
// remove our finalizer from the list and update it.
client.ObjectMeta.Finalizers = removeString(client.ObjectMeta.Finalizers, FinalizerName)
if err := r.Update(ctx, &client); err != nil {
return ctrl.Result{}, err
}
log.Info("Deleted the client")
}
// done
return ctrl.Result{}, nil
}
// Its not being deleted, so we seize the moment to take ownership.
if !containsString(client.ObjectMeta.Finalizers, FinalizerName) {
typeMeta := client.TypeMeta
client.ObjectMeta.Finalizers = append(client.ObjectMeta.Finalizers, FinalizerName)
if err := r.Update(ctx, &client); err != nil {
return ctrl.Result{}, err
}
// restore the TypeMeta object as it is removed during Update, but need to be accessed later
client.TypeMeta = typeMeta
}
// Convert Client
keycloakClient, err := ConvertToClient(client)
if err != nil {
log.Error(err, "Could not convert client")
return ctrl.Result{}, err
}
err = r.Keycloak.CreateClient(ctx, client.Spec.RealmName, keycloakClient)
if err != nil {
// try updating instead
err := r.Keycloak.UpdateClient(ctx, client.Spec.RealmName, keycloakClient)
if err != nil {
log.Error(err, "Could not create/update client")
return ctrl.Result{}, err
}
log.Info("Updated the client")
return ctrl.Result{}, nil
}
client.Status.Available = true
r.Status().Update(ctx, &client)
log.Info("Successfully created client")
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }

View file

@ -0,0 +1,131 @@
package controllers
import (
keycloakv1alpha1 "git.zom.bi/images/keycloak-operator/api/v1alpha1"
"github.com/Nerzal/gocloak/v7"
)
// ConvertToRealm takes a CRD representation and converts it into a datatype
// that can be understood by GoCloak.
func ConvertToRealm(realmCrd keycloakv1alpha1.KeycloakRealm) (gocloak.RealmRepresentation, error) {
var realm gocloak.RealmRepresentation
realmSpec := realmCrd.Spec
// Mandatory Properties
realm.Realm = &realmSpec.RealmName
// Optional Properties
realm.Enabled = realmSpec.Enabled
realm.DisplayName = realmSpec.DisplayName
realm.DisplayNameHTML = realmSpec.DisplayNameHTML
realm.LoginTheme = realmSpec.LoginTheme
realm.LoginWithEmailAllowed = realmSpec.LoginWithEmailAllowed
realm.RegistrationAllowed = realmSpec.RegistrationAllowed
realm.EditUsernameAllowed = realmSpec.EditUsernameAllowed
realm.RegistrationEmailAsUsername = realmSpec.RegistrationEmailAsUsername
realm.ResetPasswordAllowed = realmSpec.ResetPasswordAllowed
realm.DuplicateEmailsAllowed = realmSpec.DuplicateEmailsAllowed
realm.VerifyEmail = realmSpec.VerifyEmail
realm.RememberMe = realmSpec.RememberMe
if realmSpec.SMTP != nil {
smtp := map[string]string{}
if realmSpec.SMTP.Auth {
smtp["auth"] = "true"
}
if realmSpec.SMTP.Secret != nil {
// TODO
}
if realmSpec.SMTP.From != "" {
smtp["from"] = realmSpec.SMTP.From
}
realm.SMTPServer = &smtp
}
//realm.AccessCodeLifespan = ""
//realm.AccessCodeLifespanLogin = ""
//realm.AccessCodeLifespanUserAction = ""
//realm.AccessTokenLifespan = ""
//realm.AccessTokenLifespanForImplicitFlow = ""
//realm.AccountTheme = ""
//realm.ActionTokenGeneratedByAdminLifespan = ""
//realm.ActionTokenGeneratedByUserLifespan = ""
//realm.AdminEventsDetailsEnabled = ""
//realm.AdminEventsEnabled = ""
//realm.AdminTheme = ""
//realm.Attributes = ""
//realm.AuthenticationFlows = ""
//realm.AuthenticatorConfig = ""
//realm.BrowserFlow = ""
//realm.BrowserSecurityHeaders = ""
//realm.BruteForceProtected = ""
//realm.ClientAuthenticationFlow = ""
//realm.ClientScopeMappings = ""
//realm.ClientScopes = ""
//realm.Clients = ""
//realm.Components = ""
//realm.DefaultDefaultClientScopes = ""
//realm.DefaultGroups = ""
//realm.DefaultLocale = ""
//realm.DefaultOptionalClientScopes = ""
//realm.DefaultRoles = ""
//realm.DefaultSignatureAlgorithm = ""
//realm.DirectGrantFlow = ""
//realm.DockerAuthenticationFlow = ""
//realm.EmailTheme = ""
//realm.EnabledEventTypes = ""
//realm.EventsEnabled = ""
//realm.EventsExpiration = ""
//realm.EventsListeners = ""
//realm.FailureFactor = ""
//realm.FederatedUsers = ""
//realm.Groups = ""
//realm.ID = ""
//realm.IdentityProviderMappers = ""
//realm.IdentityProviders = ""
//realm.InternationalizationEnabled = ""
//realm.KeycloakVersion = ""
//realm.MaxDeltaTimeSeconds = ""
//realm.MaxFailureWaitSeconds = ""
//realm.MinimumQuickLoginWaitSeconds = ""
//realm.NotBefore = ""
//realm.OfflineSessionIdleTimeout = ""
//realm.OfflineSessionMaxLifespan = ""
//realm.OfflineSessionMaxLifespanEnabled = ""
//realm.OtpPolicyAlgorithm = ""
//realm.OtpPolicyDigits = ""
//realm.OtpPolicyInitialCounter = ""
//realm.OtpPolicyLookAheadWindow = ""
//realm.OtpPolicyPeriod = ""
//realm.OtpPolicyType = ""
//realm.OtpSupportedApplications = ""
//realm.PasswordPolicy = ""
//realm.PermanentLockout = ""
//realm.ProtocolMappers = ""
//realm.QuickLoginCheckMilliSeconds = ""
//realm.RefreshTokenMaxReuse = ""
//realm.RegistrationFlow = ""
//realm.RequiredActions = ""
//realm.ResetCredentialsFlow = ""
//realm.RevokeRefreshToken = ""
//realm.Roles = ""
//realm.ScopeMappings = ""
//realm.SMTPServer = ""
//realm.SslRequired = ""
//realm.SsoSessionIdleTimeout = ""
//realm.SsoSessionIdleTimeoutRememberMe = ""
//realm.SsoSessionMaxLifespan = ""
//realm.SsoSessionMaxLifespanRememberMe = ""
//realm.SupportedLocales = ""
//realm.UserFederationMappers = ""
//realm.UserFederationProviders = ""
//realm.UserManagedAccessAllowed = ""
//realm.Users = ""
//realm.WaitIncrementSeconds = ""
return realm, nil
}

View file

@ -19,6 +19,7 @@ package controllers
import ( import (
"context" "context"
"github.com/Nerzal/gocloak/v7"
"github.com/go-logr/logr" "github.com/go-logr/logr"
apierrs "k8s.io/apimachinery/pkg/api/errors" apierrs "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -55,29 +56,75 @@ func (r *KeycloakRealmReconciler) Reconcile(ctx context.Context, req ctrl.Reques
log.Info("reconciling") log.Info("reconciling")
// We get the information from the CRD
var realm keycloakv1alpha1.KeycloakRealm var realm keycloakv1alpha1.KeycloakRealm
if err := r.Get(ctx, req.NamespacedName, &realm); err != nil { if err := r.Get(ctx, req.NamespacedName, &realm); err != nil {
if apierrs.IsNotFound(err) { if apierrs.IsNotFound(err) {
log.Info("I would now unregister the realm") // Realm is already deleted via finalizer.
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
} }
if realm.Status.Available { if !realm.ObjectMeta.DeletionTimestamp.IsZero() {
// try to get existing realm // is in the process of being deleted
log.Info("Would try to fetch the realm by its id.", if containsString(realm.ObjectMeta.Finalizers, FinalizerName) {
"id", realm.Spec.RealmName) // our finalizer is present, so lets handle any external dependency
// if found {
log.Info("will act like i found it, updating.") // We do not want to delete anything, so we just disable the realm.
// update() disabled := gocloak.RealmRepresentation{Realm: &realm.Spec.RealmName, Enabled: gocloak.BoolP(false)}
err := r.Keycloak.UpdateRealm(ctx, disabled)
if err != nil {
// if fail to delete the external dependency here, return with error
// so that it can be retried
return ctrl.Result{}, err
}
// remove our finalizer from the list and update it.
realm.ObjectMeta.Finalizers = removeString(realm.ObjectMeta.Finalizers, FinalizerName)
if err := r.Update(ctx, &realm); err != nil {
return ctrl.Result{}, err
}
log.Info("Deleted the realm")
}
// done
return ctrl.Result{}, nil return ctrl.Result{}, nil
// }
} }
log.Info("Would now create the realm.") // Its not being deleted, so we seize the moment to take ownership.
if !containsString(realm.ObjectMeta.Finalizers, FinalizerName) {
typeMeta := realm.TypeMeta
realm.ObjectMeta.Finalizers = append(realm.ObjectMeta.Finalizers, FinalizerName)
if err := r.Update(ctx, &realm); err != nil {
return ctrl.Result{}, err
}
// restore the TypeMeta object as it is removed during Update, but need to be accessed later
realm.TypeMeta = typeMeta
}
// Convert Realm
keycloakRealm, err := ConvertToRealm(realm)
if err != nil {
log.Error(err, "Could not convert realm")
return ctrl.Result{}, err
}
err = r.Keycloak.CreateRealm(ctx, keycloakRealm)
if err != nil {
// try updating instead
err := r.Keycloak.UpdateRealm(ctx, keycloakRealm)
if err != nil {
log.Error(err, "Could not create/update realm")
return ctrl.Result{}, err
}
log.Info("Updated the realm")
return ctrl.Result{}, nil
}
realm.Status.Available = true realm.Status.Available = true
r.Status().Update(ctx, &realm) r.Status().Update(ctx, &realm)
log.Info("Successfully created realm")
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }

4
go.sum
View file

@ -45,8 +45,10 @@ github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdko
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -338,6 +340,7 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -589,6 +592,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=