mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-09-22 10:33:31 +08:00
feat: watch for both ingresses and tinyauth crds
This commit is contained in:
@@ -12,12 +12,8 @@ import (
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// LabelProvider looks up the apps it knows about for the given domain. A
|
||||
// provider that knows which hosts its apps are served on MUST only yield the
|
||||
// ones that are actually served on domain, so that an unrelated app cannot
|
||||
// claim it by name.
|
||||
type LabelProvider interface {
|
||||
Lookup(domain string, locator func(name string, app *model.App) bool) error
|
||||
Lookup(locator func(name string, app *model.App) bool) error
|
||||
}
|
||||
|
||||
type AccessControlsService struct {
|
||||
@@ -150,7 +146,7 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A
|
||||
// If we have a label provider configured, try to get ACLs from it
|
||||
if service.labelProvider != nil {
|
||||
return service.getACLs(domain, func(locator func(name string, app *model.App) bool) error {
|
||||
return service.labelProvider.Lookup(domain, locator)
|
||||
return service.labelProvider.Lookup(locator)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"github.com/tinyauthapp/tinyauth/pkg/apis/tinyauth/v1alpha1"
|
||||
)
|
||||
|
||||
type KubernetesCRDInput struct {
|
||||
Log *logger.Logger
|
||||
}
|
||||
|
||||
type KubernetesCRDExtractor struct {
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewKubernetesCRDExtractor(i KubernetesCRDInput) *KubernetesCRDExtractor {
|
||||
return &KubernetesCRDExtractor{
|
||||
log: i.Log,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubernetesCRDExtractor) Extract(app *v1alpha1.Application) ExtractionResult {
|
||||
meta := &ResourceMeta{
|
||||
Name: app.GetName(),
|
||||
Namespace: app.GetNamespace(),
|
||||
}
|
||||
|
||||
if !ensureResourceMeta(meta) {
|
||||
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("Resource has no namespace or name, skipping")
|
||||
return ExtractionResult{}
|
||||
}
|
||||
|
||||
if app.Spec.Config.Domain == "" {
|
||||
k.log.App.Warn().Str("name", meta.Name).Str("namespace", meta.Namespace).Msg("Application has no domain, skipping")
|
||||
return ExtractionResult{
|
||||
Meta: meta,
|
||||
Apps: nil,
|
||||
}
|
||||
}
|
||||
|
||||
if !ensureAscii(app.Spec.Config.Domain) {
|
||||
k.log.App.Warn().Str("name", meta.Name).Str("namespace", meta.Namespace).Str("domain", app.Spec.Config.Domain).Msg("Domain is invalid, skipping")
|
||||
return ExtractionResult{
|
||||
Meta: meta,
|
||||
Apps: nil,
|
||||
}
|
||||
}
|
||||
|
||||
return ExtractionResult{
|
||||
Meta: meta,
|
||||
Apps: &map[string]model.App{
|
||||
// Convert the CRD to the internal representation
|
||||
meta.Name: app.Spec.ToInternalApp(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,31 @@ package service
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
networking "k8s.io/api/networking/v1"
|
||||
)
|
||||
|
||||
func hostMatchesHostname(host string, hostname string) bool {
|
||||
host = normalizeDomain(host)
|
||||
hostname = normalizeDomain(hostname)
|
||||
if suffix, ok := strings.CutPrefix(host, "*."); ok {
|
||||
return strings.HasSuffix(hostname, "."+suffix)
|
||||
}
|
||||
return host == hostname
|
||||
}
|
||||
|
||||
func hostCoversName(host string, name string) bool {
|
||||
host = strings.ToLower(host)
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(host, strings.ToLower(name+"."))
|
||||
}
|
||||
|
||||
type KubernetesIngressExtractor struct {
|
||||
log *logger.Logger
|
||||
}
|
||||
@@ -54,15 +74,60 @@ func (k *KubernetesIngressExtractor) getHosts(rules []networking.IngressRule) []
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (k *KubernetesIngressExtractor) Extract(ingress *networking.Ingress) *ExtractionResult {
|
||||
func (k *KubernetesIngressExtractor) Extract(ingress *networking.Ingress) ExtractionResult {
|
||||
meta := &ResourceMeta{
|
||||
Name: ingress.GetName(),
|
||||
Namespace: ingress.GetNamespace(),
|
||||
}
|
||||
|
||||
if !ensureResourceMeta(meta) {
|
||||
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("Resource has no namespace or name, skipping")
|
||||
return ExtractionResult{}
|
||||
}
|
||||
|
||||
annotations := ingress.GetAnnotations()
|
||||
hosts := k.getHosts(ingress.Spec.Rules)
|
||||
|
||||
return &ExtractionResult{
|
||||
typ: ResourceTypeIngress,
|
||||
name: ingress.GetName(),
|
||||
namespace: ingress.GetNamespace(),
|
||||
hosts: hosts,
|
||||
annotations: annotations,
|
||||
if len(hosts) == 0 {
|
||||
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("No hosts found in resource, skipping")
|
||||
return ExtractionResult{
|
||||
Meta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
|
||||
if err != nil {
|
||||
k.log.App.Warn().Err(err).Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("Failed to decode resource labels, skipping")
|
||||
return ExtractionResult{
|
||||
Meta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
apps := make(map[string]model.App)
|
||||
|
||||
for name, config := range labels.Apps {
|
||||
if config.Config.Domain != "" {
|
||||
if !ensureAscii(config.Config.Domain) {
|
||||
k.log.App.Warn().Err(err).Str("namespace", meta.Namespace).Str("name", meta.Name).Str("domain", config.Config.Domain).Msg("Domain is invalid, matching will rely on app name")
|
||||
} else {
|
||||
if slices.ContainsFunc(hosts, func(host string) bool {
|
||||
return hostMatchesHostname(host, config.Config.Domain)
|
||||
}) {
|
||||
apps[name] = config
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if slices.ContainsFunc(hosts, func(host string) bool {
|
||||
return hostCoversName(host, name)
|
||||
}) {
|
||||
apps[name] = config
|
||||
}
|
||||
}
|
||||
|
||||
return ExtractionResult{
|
||||
Meta: meta,
|
||||
Apps: &apps,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,13 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/ding"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"github.com/tinyauthapp/tinyauth/pkg/apis/tinyauth/v1alpha1"
|
||||
"go.uber.org/dig"
|
||||
networking "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -32,10 +30,25 @@ func (w watchedResource) pretty() string {
|
||||
return w.gvr.Group + "/" + w.gvr.Version + "/" + w.gvr.Resource
|
||||
}
|
||||
|
||||
func ensureResourceMeta(meta *ResourceMeta) bool {
|
||||
return meta.Name != "" && meta.Namespace != ""
|
||||
}
|
||||
|
||||
type ResourceMeta struct {
|
||||
Name string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
type ExtractionResult struct {
|
||||
Meta *ResourceMeta
|
||||
Apps *map[string]model.App
|
||||
}
|
||||
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
ResourceTypeIngress ResourceType = "ingress"
|
||||
ResourceTypeCRD ResourceType = "crd"
|
||||
)
|
||||
|
||||
var supportedResources = []watchedResource{
|
||||
@@ -49,34 +62,10 @@ var supportedResources = []watchedResource{
|
||||
},
|
||||
}
|
||||
|
||||
func hostMatchesHostname(host string, hostname string) bool {
|
||||
host = normalizeDomain(host)
|
||||
hostname = normalizeDomain(hostname)
|
||||
if suffix, ok := strings.CutPrefix(host, "*."); ok {
|
||||
return strings.HasSuffix(hostname, "."+suffix)
|
||||
}
|
||||
return host == hostname
|
||||
}
|
||||
|
||||
func hostCoversName(host string, name string) bool {
|
||||
host = strings.ToLower(host)
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(host, strings.ToLower(name+"."))
|
||||
}
|
||||
|
||||
type ExtractionResult struct {
|
||||
typ ResourceType
|
||||
name string
|
||||
namespace string
|
||||
hosts []string
|
||||
annotations map[string]string
|
||||
}
|
||||
|
||||
type typedItem struct {
|
||||
typ ResourceType
|
||||
ingress *networking.Ingress
|
||||
crd *v1alpha1.Application
|
||||
}
|
||||
|
||||
func convertFromUnstructured[T any](obj *unstructured.Unstructured) (*T, error) {
|
||||
@@ -105,33 +94,13 @@ func (ti *typedItem) fromUnstructured(typ ResourceType, obj *unstructured.Unstru
|
||||
}
|
||||
}
|
||||
|
||||
type resourceEntry struct {
|
||||
name string
|
||||
app model.App
|
||||
}
|
||||
|
||||
type routedApps struct {
|
||||
hosts []string
|
||||
entries []resourceEntry
|
||||
}
|
||||
|
||||
type resourceKey struct {
|
||||
typ ResourceType
|
||||
namespace string
|
||||
name string
|
||||
}
|
||||
|
||||
type KubernetesService struct {
|
||||
log *logger.Logger
|
||||
|
||||
apps map[resourceKey]routedApps
|
||||
apps map[ResourceMeta]map[string]model.App
|
||||
client dynamic.Interface
|
||||
mu sync.RWMutex
|
||||
connected bool
|
||||
|
||||
extractors struct {
|
||||
ingress *KubernetesIngressExtractor
|
||||
}
|
||||
}
|
||||
|
||||
type KubernetesServiceInput struct {
|
||||
@@ -156,13 +125,9 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error)
|
||||
service := &KubernetesService{
|
||||
log: i.Log,
|
||||
client: client,
|
||||
apps: make(map[resourceKey]routedApps),
|
||||
apps: make(map[ResourceMeta]map[string]model.App),
|
||||
}
|
||||
|
||||
service.extractors.ingress = NewKubernetesIngressExtractor(KubernetesIngressExtractorInput{
|
||||
Log: i.Log,
|
||||
})
|
||||
|
||||
watchedGVRs := make(map[string]bool)
|
||||
|
||||
for _, res := range supportedResources {
|
||||
@@ -171,8 +136,7 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
// The Gateway API CRDs are not installed on every cluster, so a
|
||||
// single unreachable resource is not fatal
|
||||
// The CRD may not be available yet, so we'll fall back to ingress
|
||||
i.Log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to access resource, skipping watcher")
|
||||
continue
|
||||
}
|
||||
@@ -196,39 +160,25 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error)
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesService) addResourceEntries(key resourceKey, hosts []string, entries []resourceEntry) {
|
||||
func (k *KubernetesService) addResource(result ExtractionResult) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.apps[key] = routedApps{
|
||||
hosts: hosts,
|
||||
entries: entries,
|
||||
}
|
||||
k.apps[*result.Meta] = *result.Apps
|
||||
}
|
||||
|
||||
func (k *KubernetesService) removeResource(key resourceKey) {
|
||||
func (k *KubernetesService) removeResource(meta ResourceMeta) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
delete(k.apps, key)
|
||||
delete(k.apps, meta)
|
||||
}
|
||||
|
||||
func (k *KubernetesService) getEntry(domain string, locator func(name string, app *model.App) bool) {
|
||||
if !ensureAscii(domain) {
|
||||
k.log.App.Debug().Str("domain", domain).Msg("Domain is invalid, skipping lookup")
|
||||
return
|
||||
}
|
||||
|
||||
func (k *KubernetesService) getEntry(locator func(name string, app *model.App) bool) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
// O(n^2) is not great but the number of resource entries is expected to be small
|
||||
for _, app := range k.apps {
|
||||
if !slices.ContainsFunc(app.hosts, func(host string) bool {
|
||||
return hostMatchesHostname(host, domain)
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
for _, entry := range app.entries {
|
||||
if ok := locator(entry.name, &entry.app); ok {
|
||||
for _, apps := range k.apps {
|
||||
for name, app := range apps {
|
||||
if ok := locator(name, &app); ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -236,81 +186,43 @@ func (k *KubernetesService) getEntry(domain string, locator func(name string, ap
|
||||
}
|
||||
|
||||
func (k *KubernetesService) updateFromItem(res watchedResource, typedItem *typedItem) {
|
||||
var result *ExtractionResult
|
||||
|
||||
if typedItem == nil {
|
||||
k.log.App.Warn().Str("res", res.pretty()).Msg("Resource is nil, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
var result ExtractionResult
|
||||
|
||||
switch typedItem.typ {
|
||||
case ResourceTypeIngress:
|
||||
if typedItem.ingress == nil {
|
||||
k.log.App.Warn().Str("res", res.pretty()).Msg("Ingress is nil, skipping")
|
||||
return
|
||||
}
|
||||
result = k.extractors.ingress.Extract(typedItem.ingress)
|
||||
extractor := NewKubernetesIngressExtractor(KubernetesIngressExtractorInput{
|
||||
Log: k.log,
|
||||
})
|
||||
result = extractor.Extract(typedItem.ingress)
|
||||
case ResourceTypeCRD:
|
||||
if typedItem.crd == nil {
|
||||
k.log.App.Warn().Str("res", res.pretty()).Msg("CRD is nil, skipping")
|
||||
return
|
||||
}
|
||||
extractor := NewKubernetesCRDExtractor(KubernetesCRDInput{
|
||||
Log: k.log,
|
||||
})
|
||||
result = extractor.Extract(typedItem.crd)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
if result.Apps == nil {
|
||||
k.log.App.Warn().Str("res", res.pretty()).Msg("Failed to extract resource, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
key := resourceKey{
|
||||
typ: res.typ,
|
||||
namespace: result.namespace,
|
||||
name: result.name,
|
||||
}
|
||||
|
||||
if len(result.hosts) == 0 {
|
||||
k.log.App.Warn().Str("res", res.pretty()).Str("namespace", key.namespace).Str("name", key.name).Msg("No hosts found in resource, skipping")
|
||||
k.removeResource(key)
|
||||
return
|
||||
}
|
||||
|
||||
labels, err := decoders.DecodeLabels[model.Apps](result.annotations, "apps")
|
||||
if err != nil {
|
||||
k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Msg("Failed to decode resource labels, skipping")
|
||||
k.removeResource(key)
|
||||
return
|
||||
}
|
||||
|
||||
var entries []resourceEntry
|
||||
|
||||
for name, config := range labels.Apps {
|
||||
if config.Config.Domain != "" {
|
||||
if !ensureAscii(config.Config.Domain) {
|
||||
k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Str("domain", config.Config.Domain).Msg("Domain is invalid, matching will rely on app name")
|
||||
} else {
|
||||
if slices.ContainsFunc(result.hosts, func(host string) bool {
|
||||
return hostMatchesHostname(host, config.Config.Domain)
|
||||
}) {
|
||||
entries = append(entries, resourceEntry{
|
||||
name: name,
|
||||
app: config,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
if result.Meta != nil {
|
||||
k.removeResource(*result.Meta)
|
||||
}
|
||||
|
||||
if slices.ContainsFunc(result.hosts, func(host string) bool {
|
||||
return hostCoversName(host, name)
|
||||
}) {
|
||||
entries = append(entries, resourceEntry{
|
||||
name: name,
|
||||
app: config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
k.removeResource(key)
|
||||
return
|
||||
}
|
||||
|
||||
k.addResourceEntries(key, result.hosts, entries)
|
||||
k.addResource(result)
|
||||
}
|
||||
|
||||
func (k *KubernetesService) resyncGVR(res watchedResource, ctx context.Context) error {
|
||||
@@ -358,14 +270,8 @@ func (k *KubernetesService) runWatcher(res watchedResource, w watch.Interface, r
|
||||
continue
|
||||
}
|
||||
switch event.Type {
|
||||
case watch.Added, watch.Modified:
|
||||
case watch.Added, watch.Modified, watch.Deleted:
|
||||
k.updateFromItem(res, newTypedItem)
|
||||
case watch.Deleted:
|
||||
k.removeResource(resourceKey{
|
||||
typ: res.typ,
|
||||
namespace: item.GetNamespace(),
|
||||
name: item.GetName(),
|
||||
})
|
||||
}
|
||||
case <-resyncTicker.C:
|
||||
if err := k.resyncGVR(res, ctx); err != nil {
|
||||
@@ -412,13 +318,13 @@ func (k *KubernetesService) watchGVR(res watchedResource, ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubernetesService) Lookup(domain string, locator func(name string, app *model.App) bool) error {
|
||||
func (k *KubernetesService) Lookup(locator func(name string, app *model.App) bool) error {
|
||||
if !k.connected {
|
||||
k.log.App.Debug().Msg("Kubernetes label provider not started, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
k.getEntry(domain, locator)
|
||||
k.getEntry(locator)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,23 +6,11 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// ApplicationSet is a list of Application resources
|
||||
type ApplicationSet struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []Application `json:"items"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// Application is a set of access control rules that can be applied to a
|
||||
// specific domain. It is an alternative to environment variable or config-based
|
||||
// access controls.
|
||||
type Application struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ApplicationSpec `json:"spec,omitempty"`
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
)
|
||||
|
||||
// ToInternalApp converts the ApplicationSpec to the internal App structure
|
||||
func (s *ApplicationSpec) ToInternalApp() model.App {
|
||||
return model.App{
|
||||
Config: model.AppConfig{
|
||||
Domain: s.Config.Domain,
|
||||
},
|
||||
Users: model.AppUsers{
|
||||
Allow: s.Users.Allow,
|
||||
Block: s.Users.Block,
|
||||
},
|
||||
OAuth: model.AppOAuth{
|
||||
Whitelist: s.OAuth.Whitelist,
|
||||
Groups: s.OAuth.Groups,
|
||||
},
|
||||
IP: model.AppIP{
|
||||
Allow: s.IP.Allow,
|
||||
Block: s.IP.Block,
|
||||
Bypass: s.IP.Bypass,
|
||||
},
|
||||
Response: model.AppResponse{
|
||||
Headers: s.Response.Headers,
|
||||
BasicAuth: model.AppBasicAuth{
|
||||
Username: s.Response.BasicAuth.Username,
|
||||
Password: s.Response.BasicAuth.Password,
|
||||
PasswordFile: s.Response.BasicAuth.PasswordFile,
|
||||
},
|
||||
},
|
||||
Path: model.AppPath{
|
||||
Allow: s.Path.Allow,
|
||||
Block: s.Path.Block,
|
||||
},
|
||||
LDAP: model.AppLDAP{
|
||||
Groups: s.LDAP.Groups,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -175,38 +175,6 @@ func (in *Application) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApplicationSet) DeepCopyInto(out *ApplicationSet) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Application, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSet.
|
||||
func (in *ApplicationSet) DeepCopy() *ApplicationSet {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApplicationSet)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ApplicationSet) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) {
|
||||
*out = *in
|
||||
|
||||
Reference in New Issue
Block a user