LOG-1251/convert to statefulsets (#16)

pull/4881/head
Brett Jones 4 years ago committed by GitHub
parent b31c3316fe
commit 2ee3b4649c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      api/v1beta1/lokistack_types.go
  2. 3
      bundle/manifests/loki-operator.clusterserviceversion.yaml
  3. 3
      bundle/manifests/loki.openshift.io_lokistacks.yaml
  4. 4
      cmd/loki-broker/main.go
  5. 3
      config/crd/bases/loki.openshift.io_lokistacks.yaml
  6. 1
      config/rbac/role.yaml
  7. 3
      config/samples/loki_v1beta1_lokistack.yaml
  8. 2
      controllers/lokistack_controller.go
  9. 2
      go.mod
  10. 2
      go.sum
  11. 1
      internal/handlers/lokistack_create.go
  12. 2
      internal/handlers/lokistack_create_test.go
  13. 32
      internal/manifests/build.go
  14. 9
      internal/manifests/config.go
  15. 88
      internal/manifests/distributor.go
  16. 148
      internal/manifests/ingester.go
  17. 31
      internal/manifests/ingester_test.go
  18. 13
      internal/manifests/memberlist.go
  19. 6
      internal/manifests/options.go
  20. 134
      internal/manifests/querier.go
  21. 31
      internal/manifests/querier_test.go
  22. 88
      internal/manifests/query-frontend.go
  23. 1
      vendor/github.com/creasty/defaults/.gitignore
  24. 20
      vendor/github.com/creasty/defaults/.travis.yml
  25. 22
      vendor/github.com/creasty/defaults/LICENSE
  26. 30
      vendor/github.com/creasty/defaults/Makefile
  27. 69
      vendor/github.com/creasty/defaults/README.md
  28. 195
      vendor/github.com/creasty/defaults/defaults.go
  29. 3
      vendor/github.com/creasty/defaults/go.mod
  30. 12
      vendor/github.com/creasty/defaults/setter.go
  31. 4
      vendor/modules.txt

@ -28,8 +28,7 @@ type LokiStackSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Name is the unique name of this LokiStack.
Name string `json:"name,omitempty"`
StorageClassName string `json:"storage_class_name,omitempty"`
}
// LokiStackStatus defines the observed state of LokiStack

@ -11,7 +11,7 @@ metadata:
"name": "lokistack-sample"
},
"spec": {
"name": "mystack"
"storage_class_name": "standard"
}
}
]
@ -58,6 +58,7 @@ spec:
- apps
resources:
- deployments
- statefulsets
verbs:
- create
- delete

@ -30,8 +30,7 @@ spec:
spec:
description: LokiStackSpec defines the desired state of LokiStack
properties:
name:
description: Name is the unique name of this LokiStack.
storage_class_name:
type: string
type: object
status:

@ -17,7 +17,7 @@ import (
type config struct {
Name string
Namespace string
Replicas int `yaml:"replicas"`
Replicas int
writeToDir string
}
@ -68,7 +68,7 @@ func main() {
if cfg.writeToDir != "" {
basename := fmt.Sprintf("%s-%s.yaml", o.GetObjectKind().GroupVersionKind().Kind, o.GetName())
fname := strings.ToLower(path.Join(cfg.writeToDir, basename))
if err := ioutil.WriteFile(fname, b, 0644); err != nil {
if err := ioutil.WriteFile(fname, b, 0o644); err != nil {
log.Error(err, "failed to write file to directory", "path", fname)
os.Exit(1)
}

@ -36,8 +36,7 @@ spec:
spec:
description: LokiStackSpec defines the desired state of LokiStack
properties:
name:
description: Name is the unique name of this LokiStack.
storage_class_name:
type: string
type: object
status:

@ -26,6 +26,7 @@ rules:
- apps
resources:
- deployments
- statefulsets
verbs:
- create
- delete

@ -3,5 +3,4 @@ kind: LokiStack
metadata:
name: lokistack-sample
spec:
# Add fields here
name: mystack
storage_class_name: "standard" # this is the default for KiND

@ -42,7 +42,7 @@ type LokiStackReconciler struct {
// +kubebuilder:rbac:groups=loki.openshift.io,resources=lokistacks/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=loki.openshift.io,resources=lokistacks/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=pods;nodes;services;endpoints;configmaps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments;statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings;clusterroles,verbs=get;list;watch;create;update;patch;delete
// Reconcile is part of the main kubernetes reconciliation loop which aims to

@ -4,6 +4,7 @@ go 1.16
require (
github.com/ViaQ/logerr v1.0.9
github.com/creasty/defaults v1.5.1
github.com/go-logr/logr v0.4.0
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0
github.com/stretchr/testify v1.6.1
@ -12,4 +13,5 @@ require (
k8s.io/client-go v0.20.4
k8s.io/utils v0.0.0-20210111153108-fddb29f9d009
sigs.k8s.io/controller-runtime v0.8.3
sigs.k8s.io/yaml v1.2.0
)

@ -84,6 +84,8 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creasty/defaults v1.5.1 h1:j8WexcS3d/t4ZmllX4GEkl4wIB/trOr035ajcLHCISM=
github.com/creasty/defaults v1.5.1/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

@ -33,6 +33,7 @@ func CreateLokiStack(ctx context.Context, req ctrl.Request, k k8s.Client) error
opts := manifests.Options{
Name: req.Name,
Namespace: req.Namespace,
Stack: stack.Spec,
}
ll.Info("begin building manifests")

@ -61,7 +61,6 @@ func TestCreateLokiStack_WhenGetReturnsNotFound_DoesNotError(t *testing.T) {
require.Zero(t, k.CreateCallCount())
}
func TestCreateLokiStack_WhenGetReturnsAnErrorOtherThanNotFound_ReturnsTheError(t *testing.T) {
k := &k8sfakes.FakeClient{}
r := ctrl.Request{
@ -84,7 +83,6 @@ func TestCreateLokiStack_WhenGetReturnsAnErrorOtherThanNotFound_ReturnsTheError(
require.Zero(t, k.CreateCallCount())
}
func TestCreateLokiStack_SetsNamespaceOnAllObjects(t *testing.T) {
k := &k8sfakes.FakeClient{}
r := ctrl.Request{

@ -1,23 +1,51 @@
package manifests
import (
"github.com/ViaQ/logerr/kverrors"
"github.com/creasty/defaults"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// BuildAll builds all manifests required to run a Loki Stack
func BuildAll(opt Options) ([]client.Object, error) {
if err := setDefaultOptions(&opt); err != nil {
return nil, err
}
res := make([]client.Object, 0)
cm, err := LokiConfigMap(opt.Name, opt.Namespace)
if err != nil {
return nil, err
}
res = append(res, cm)
res = append(res, BuildDistributor(opt.Name)...)
res = append(res, BuildIngester(opt.Name)...)
res = append(res, BuildQuerier(opt.Name)...)
ingesterObjects, err := BuildIngester(opt)
if err != nil {
return nil, err
}
res = append(res, ingesterObjects...)
querierObjects, err := BuildQuerier(opt)
if err != nil {
return nil, err
}
res = append(res, querierObjects...)
res = append(res, BuildQueryFrontend(opt.Name)...)
res = append(res, LokiGossipRingService(opt.Name))
return res, nil
}
func setDefaultOptions(o *Options) error {
if err := defaults.Set(o); err != nil {
// I believe this is only caused when the default tag has an incorrect type such as Field int `default:"hello"`
// so it shouldn't happen in production
return kverrors.Wrap(err, "could not set default options")
}
// TODO add some complex defaults here that the vendored package does not support
return nil
}

@ -5,13 +5,12 @@ import (
"strings"
"github.com/ViaQ/loki-operator/internal/manifests/internal/config"
apps "k8s.io/api/apps/v1"
core "k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// LokiConfigMap creates the single configmap containing the loki configuration for the whole cluster
func LokiConfigMap(stackName, namespace string) (*core.ConfigMap, error) {
func LokiConfigMap(stackName, namespace string) (*corev1.ConfigMap, error) {
b, err := config.Build(config.Options{
FrontendWorker: config.Address{
FQDN: "",
@ -32,10 +31,10 @@ func LokiConfigMap(stackName, namespace string) (*core.ConfigMap, error) {
return nil, err
}
return &core.ConfigMap{
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: lokiConfigMapName(stackName),

@ -5,8 +5,8 @@ import (
"path"
"github.com/ViaQ/loki-operator/internal/manifests/internal/config"
apps "k8s.io/api/apps/v1"
core "k8s.io/api/core/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/intstr"
@ -30,14 +30,14 @@ func BuildDistributor(stackName string) []client.Object {
}
// NewDistributorDeployment creates a deployment object for a distributor
func NewDistributorDeployment(stackName string) *apps.Deployment {
podSpec := core.PodSpec{
Volumes: []core.Volume{
func NewDistributorDeployment(stackName string) *appsv1.Deployment {
podSpec := corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: configVolumeName,
VolumeSource: core.VolumeSource{
ConfigMap: &core.ConfigMapVolumeSource{
LocalObjectReference: core.LocalObjectReference{
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: lokiConfigMapName(stackName),
},
},
@ -45,12 +45,12 @@ func NewDistributorDeployment(stackName string) *apps.Deployment {
},
{
Name: storageVolumeName,
VolumeSource: core.VolumeSource{
EmptyDir: &core.EmptyDirVolumeSource{},
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
},
Containers: []core.Container{
Containers: []corev1.Container{
{
Image: containerImage,
Name: "loki-distributor",
@ -58,30 +58,30 @@ func NewDistributorDeployment(stackName string) *apps.Deployment {
"-target=distributor",
fmt.Sprintf("-config.file=%s", path.Join(config.LokiConfigMountDir, config.LokiConfigFileName)),
},
ReadinessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
ReadinessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/ready",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
InitialDelaySeconds: 15,
TimeoutSeconds: 1,
},
LivenessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
LivenessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/metrics",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
TimeoutSeconds: 2,
PeriodSeconds: 30,
FailureThreshold: 10,
},
Ports: []core.ContainerPort{
Ports: []corev1.ContainerPort{
{
Name: "metrics",
ContainerPort: httpPort,
@ -95,17 +95,17 @@ func NewDistributorDeployment(stackName string) *apps.Deployment {
ContainerPort: gossipPort,
},
},
// Resources: core.ResourceRequirements{
// Limits: core.ResourceList{
// core.ResourceMemory: resource.MustParse("1Gi"),
// core.ResourceCPU: resource.MustParse("1000m"),
// Resources: corev1.ResourceRequirements{
// Limits: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("1Gi"),
// corev1.ResourceCPU: resource.MustParse("1000m"),
// },
// Requests: core.ResourceList{
// core.ResourceMemory: resource.MustParse("50m"),
// core.ResourceCPU: resource.MustParse("50m"),
// Requests: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("50m"),
// corev1.ResourceCPU: resource.MustParse("50m"),
// },
// },
VolumeMounts: []core.VolumeMount{
VolumeMounts: []corev1.VolumeMount{
{
Name: configVolumeName,
ReadOnly: false,
@ -123,49 +123,49 @@ func NewDistributorDeployment(stackName string) *apps.Deployment {
l := ComponentLabels("distributor", stackName)
return &apps.Deployment{
return &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-distributor-%s", stackName),
Labels: l,
},
Spec: apps.DeploymentSpec{
Spec: appsv1.DeploymentSpec{
Replicas: pointer.Int32Ptr(int32(3)),
Selector: &metav1.LabelSelector{
MatchLabels: labels.Merge(l, GossipLabels()),
},
Template: core.PodTemplateSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-distributor-%s", stackName),
Labels: labels.Merge(l, GossipLabels()),
},
Spec: podSpec,
},
Strategy: apps.DeploymentStrategy{
Type: apps.RollingUpdateDeploymentStrategyType,
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
},
},
}
}
// NewDistributorHTTPService creates a k8s service for the distributor HTTP endpoint
func NewDistributorHTTPService(stackName string) *core.Service {
func NewDistributorHTTPService(stackName string) *corev1.Service {
l := ComponentLabels("distributor", stackName)
return &core.Service{
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: serviceNameDistributorGRPC(stackName),
Labels: l,
},
Spec: core.ServiceSpec{
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Ports: []core.ServicePort{
Ports: []corev1.ServicePort{
{
Name: "grpc",
Port: grpcPort,
@ -177,19 +177,19 @@ func NewDistributorHTTPService(stackName string) *core.Service {
}
// NewDistributorGRPCService creates a k8s service for the distributor GRPC endpoint
func NewDistributorGRPCService(stackName string) *core.Service {
func NewDistributorGRPCService(stackName string) *corev1.Service {
l := ComponentLabels("distributor", stackName)
return &core.Service{
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: serviceNameDistributorHTTP(stackName),
Labels: l,
},
Spec: core.ServiceSpec{
Ports: []core.ServicePort{
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "metrics",
Port: httpPort,

@ -5,8 +5,9 @@ import (
"path"
"github.com/ViaQ/loki-operator/internal/manifests/internal/config"
apps "k8s.io/api/apps/v1"
core "k8s.io/api/core/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/intstr"
@ -15,36 +16,30 @@ import (
)
// BuildIngester builds the k8s objects required to run Loki Ingester
func BuildIngester(stackName string) []client.Object {
func BuildIngester(opts Options) ([]client.Object, error) {
return []client.Object{
NewIngesterDeployment(stackName),
NewIngesterGRPCService(stackName),
NewIngesterHTTPService(stackName),
}
NewIngesterStatefulSet(opts),
NewIngesterGRPCService(opts),
NewIngesterHTTPService(opts),
}, nil
}
// NewIngesterDeployment creates a deployment object for an ingester
func NewIngesterDeployment(stackName string) *apps.Deployment {
podSpec := core.PodSpec{
Volumes: []core.Volume{
// NewIngesterStatefulSet creates a deployment object for an ingester
func NewIngesterStatefulSet(opts Options) *appsv1.StatefulSet {
podSpec := corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: configVolumeName,
VolumeSource: core.VolumeSource{
ConfigMap: &core.ConfigMapVolumeSource{
LocalObjectReference: core.LocalObjectReference{
Name: lokiConfigMapName(stackName),
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: lokiConfigMapName(opts.Name),
},
},
},
},
{
Name: storageVolumeName,
VolumeSource: core.VolumeSource{
EmptyDir: &core.EmptyDirVolumeSource{},
},
},
},
Containers: []core.Container{
Containers: []corev1.Container{
{
Image: containerImage,
Name: "loki-ingester",
@ -52,30 +47,30 @@ func NewIngesterDeployment(stackName string) *apps.Deployment {
"-target=ingester",
fmt.Sprintf("-config.file=%s", path.Join(config.LokiConfigMountDir, config.LokiConfigFileName)),
},
ReadinessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
ReadinessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/ready",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
InitialDelaySeconds: 15,
TimeoutSeconds: 1,
},
LivenessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
LivenessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/metrics",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
TimeoutSeconds: 2,
PeriodSeconds: 30,
FailureThreshold: 10,
},
Ports: []core.ContainerPort{
Ports: []corev1.ContainerPort{
{
Name: "metrics",
ContainerPort: httpPort,
@ -89,17 +84,17 @@ func NewIngesterDeployment(stackName string) *apps.Deployment {
ContainerPort: gossipPort,
},
},
// Resources: core.ResourceRequirements{
// Limits: core.ResourceList{
// core.ResourceMemory: resource.MustParse("1Gi"),
// core.ResourceCPU: resource.MustParse("1000m"),
// Resources: corev1.ResourceRequirements{
// Limits: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("1Gi"),
// corev1.ResourceCPU: resource.MustParse("1000m"),
// },
// Requests: core.ResourceList{
// core.ResourceMemory: resource.MustParse("50m"),
// core.ResourceCPU: resource.MustParse("50m"),
// Requests: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("50m"),
// corev1.ResourceCPU: resource.MustParse("50m"),
// },
// },
VolumeMounts: []core.VolumeMount{
VolumeMounts: []corev1.VolumeMount{
{
Name: configVolumeName,
ReadOnly: false,
@ -115,51 +110,70 @@ func NewIngesterDeployment(stackName string) *apps.Deployment {
},
}
l := ComponentLabels("ingester", stackName)
ingesterLabels := ComponentLabels("ingester", opts.Name)
return &apps.Deployment{
return &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: apps.SchemeGroupVersion.String(),
Kind: "StatefulSet",
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-ingester-%s", stackName),
Labels: l,
Name: fmt.Sprintf("loki-ingester-%s", opts.Name),
Labels: ingesterLabels,
},
Spec: apps.DeploymentSpec{
Replicas: pointer.Int32Ptr(int32(3)),
Spec: appsv1.StatefulSetSpec{
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
RevisionHistoryLimit: pointer.Int32Ptr(10),
Replicas: pointer.Int32Ptr(int32(3)),
Selector: &metav1.LabelSelector{
MatchLabels: labels.Merge(l, GossipLabels()),
MatchLabels: labels.Merge(ingesterLabels, GossipLabels()),
},
Template: core.PodTemplateSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-ingester-%s", stackName),
Labels: labels.Merge(l, GossipLabels()),
Name: fmt.Sprintf("loki-ingester-%s", opts.Name),
Labels: labels.Merge(ingesterLabels, GossipLabels()),
},
Spec: podSpec,
},
Strategy: apps.DeploymentStrategy{
Type: apps.RollingUpdateDeploymentStrategyType,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Labels: ingesterLabels,
Name: storageVolumeName,
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
// TODO: should we verify that this is possible with the given storage class first?
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceStorage: resource.MustParse("1Gi"),
},
},
StorageClassName: pointer.StringPtr(opts.Stack.StorageClassName),
},
},
},
},
}
}
// NewIngesterGRPCService creates a k8s service for the ingester GRPC endpoint
func NewIngesterGRPCService(stackName string) *core.Service {
l := ComponentLabels("ingester", stackName)
return &core.Service{
func NewIngesterGRPCService(opts Options) *corev1.Service {
l := ComponentLabels("ingester", opts.Name)
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: serviceNameIngesterGRPC(stackName),
Name: serviceNameIngesterGRPC(opts.Name),
Labels: l,
},
Spec: core.ServiceSpec{
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Ports: []core.ServicePort{
Ports: []corev1.ServicePort{
{
Name: "grpc",
Port: grpcPort,
@ -171,19 +185,19 @@ func NewIngesterGRPCService(stackName string) *core.Service {
}
// NewIngesterHTTPService creates a k8s service for the ingester HTTP endpoint
func NewIngesterHTTPService(stackName string) *core.Service {
l := ComponentLabels("ingester", stackName)
return &core.Service{
func NewIngesterHTTPService(opts Options) *corev1.Service {
l := ComponentLabels("ingester", opts.Name)
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: serviceNameIngesterHTTP(stackName),
Name: serviceNameIngesterHTTP(opts.Name),
Labels: l,
},
Spec: core.ServiceSpec{
Ports: []core.ServicePort{
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "metrics",
Port: httpPort,

@ -0,0 +1,31 @@
package manifests_test
import (
"testing"
lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1"
"github.com/ViaQ/loki-operator/internal/manifests"
"github.com/stretchr/testify/require"
)
func TestNewIngesterStatefulSet_SelectorMatchesLabels(t *testing.T) {
// You must set the .spec.selector field of a StatefulSet to match the labels of
// its .spec.template.metadata.labels. Prior to Kubernetes 1.8, the
// .spec.selector field was defaulted when omitted. In 1.8 and later versions,
// failing to specify a matching Pod Selector will result in a validation error
// during StatefulSet creation.
// See https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-selector
ss := manifests.NewIngesterStatefulSet(manifests.Options{
Name: "abcd",
Namespace: "efgh",
Stack: lokiv1beta1.LokiStackSpec{
StorageClassName: "standard",
},
})
l := ss.Spec.Template.GetObjectMeta().GetLabels()
for key, value := range ss.Spec.Selector.MatchLabels {
require.Contains(t, l, key)
require.Equal(t, l[key], value)
}
}

@ -3,25 +3,24 @@ package manifests
import (
"fmt"
apps "k8s.io/api/apps/v1"
core "k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// LokiGossipRingService creates a k8s service for the gossip/memberlist members of the cluster
func LokiGossipRingService(stackName string) *core.Service {
return &core.Service{
func LokiGossipRingService(stackName string) *corev1.Service {
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-gossip-ring-%s", stackName),
Labels: commonLabels(stackName),
},
Spec: core.ServiceSpec{
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Ports: []core.ServicePort{
Ports: []corev1.ServicePort{
{
Name: "gossip",
Port: gossipPort,

@ -1,9 +1,15 @@
package manifests
import (
lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1"
)
// Options is a set of options to use when building manifests such as resource sizes, etc.
// Most of this should be provided - either directly or indirectly - by the user. This will
// probably be converted from the CR.
type Options struct {
Name string
Namespace string
Stack lokiv1beta1.LokiStackSpec
}

@ -5,8 +5,9 @@ import (
"path"
"github.com/ViaQ/loki-operator/internal/manifests/internal/config"
apps "k8s.io/api/apps/v1"
core "k8s.io/api/core/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/intstr"
@ -15,36 +16,30 @@ import (
)
// BuildQuerier returns a list of k8s objects for Loki Querier
func BuildQuerier(stackName string) []client.Object {
func BuildQuerier(opts Options) ([]client.Object, error) {
return []client.Object{
NewQuerierDeployment(stackName),
NewQuerierGRPCService(stackName),
NewQuerierHTTPService(stackName),
}
NewQuerierStatefulSet(opts),
NewQuerierGRPCService(opts.Name),
NewQuerierHTTPService(opts.Name),
}, nil
}
// NewQuerierDeployment creates a deployment object for a querier
func NewQuerierDeployment(stackName string) *apps.Deployment {
podSpec := core.PodSpec{
Volumes: []core.Volume{
// NewQuerierStatefulSet creates a deployment object for a querier
func NewQuerierStatefulSet(opts Options) *appsv1.StatefulSet {
podSpec := corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: configVolumeName,
VolumeSource: core.VolumeSource{
ConfigMap: &core.ConfigMapVolumeSource{
LocalObjectReference: core.LocalObjectReference{
Name: lokiConfigMapName(stackName),
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: lokiConfigMapName(opts.Name),
},
},
},
},
{
Name: storageVolumeName,
VolumeSource: core.VolumeSource{
EmptyDir: &core.EmptyDirVolumeSource{},
},
},
},
Containers: []core.Container{
Containers: []corev1.Container{
{
Image: containerImage,
Name: "loki-querier",
@ -52,30 +47,30 @@ func NewQuerierDeployment(stackName string) *apps.Deployment {
"-target=querier",
fmt.Sprintf("-config.file=%s", path.Join(config.LokiConfigMountDir, config.LokiConfigFileName)),
},
ReadinessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
ReadinessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/ready",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
InitialDelaySeconds: 15,
TimeoutSeconds: 1,
},
LivenessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
LivenessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/metrics",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
TimeoutSeconds: 2,
PeriodSeconds: 30,
FailureThreshold: 10,
},
Ports: []core.ContainerPort{
Ports: []corev1.ContainerPort{
{
Name: "metrics",
ContainerPort: httpPort,
@ -89,17 +84,17 @@ func NewQuerierDeployment(stackName string) *apps.Deployment {
ContainerPort: gossipPort,
},
},
// Resources: core.ResourceRequirements{
// Limits: core.ResourceList{
// core.ResourceMemory: resource.MustParse("1Gi"),
// core.ResourceCPU: resource.MustParse("1000m"),
// Resources: corev1.ResourceRequirements{
// Limits: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("1Gi"),
// corev1.ResourceCPU: resource.MustParse("1000m"),
// },
// Requests: core.ResourceList{
// core.ResourceMemory: resource.MustParse("50m"),
// core.ResourceCPU: resource.MustParse("50m"),
// Requests: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("50m"),
// corev1.ResourceCPU: resource.MustParse("50m"),
// },
// },
VolumeMounts: []core.VolumeMount{
VolumeMounts: []corev1.VolumeMount{
{
Name: configVolumeName,
ReadOnly: false,
@ -115,51 +110,70 @@ func NewQuerierDeployment(stackName string) *apps.Deployment {
},
}
l := ComponentLabels("querier", stackName)
l := ComponentLabels("querier", opts.Name)
return &apps.Deployment{
return &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: apps.SchemeGroupVersion.String(),
Kind: "StatefulSet",
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-querier-%s", stackName),
Name: fmt.Sprintf("loki-querier-%s", opts.Name),
Labels: l,
},
Spec: apps.DeploymentSpec{
Replicas: pointer.Int32Ptr(int32(3)),
Spec: appsv1.StatefulSetSpec{
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
RevisionHistoryLimit: pointer.Int32Ptr(10),
Replicas: pointer.Int32Ptr(int32(3)),
Selector: &metav1.LabelSelector{
MatchLabels: labels.Merge(l, GossipLabels()),
},
Template: core.PodTemplateSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-querier-%s", stackName),
Name: fmt.Sprintf("loki-querier-%s", opts.Name),
Labels: labels.Merge(l, GossipLabels()),
},
Spec: podSpec,
},
Strategy: apps.DeploymentStrategy{
Type: apps.RollingUpdateDeploymentStrategyType,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Labels: l,
Name: storageVolumeName,
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
// TODO: should we verify that this is possible with the given storage class first?
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceStorage: resource.MustParse("1Gi"),
},
},
StorageClassName: pointer.StringPtr(opts.Stack.StorageClassName),
},
},
},
},
}
}
// NewQuerierGRPCService creates a k8s service for the querier GRPC endpoint
func NewQuerierGRPCService(stackName string) *core.Service {
func NewQuerierGRPCService(stackName string) *corev1.Service {
l := ComponentLabels("querier", stackName)
return &core.Service{
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: serviceNameQuerierGRPC(stackName),
Labels: l,
},
Spec: core.ServiceSpec{
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Ports: []core.ServicePort{
Ports: []corev1.ServicePort{
{
Name: "grpc",
Port: grpcPort,
@ -171,20 +185,20 @@ func NewQuerierGRPCService(stackName string) *core.Service {
}
// NewQuerierHTTPService creates a k8s service for the querier HTTP endpoint
func NewQuerierHTTPService(stackName string) *core.Service {
func NewQuerierHTTPService(stackName string) *corev1.Service {
l := ComponentLabels("querier", stackName)
return &core.Service{
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: serviceNameQuerierHTTP(stackName),
Labels: l,
},
Spec: core.ServiceSpec{
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Ports: []core.ServicePort{
Ports: []corev1.ServicePort{
{
Name: "http",
Port: httpPort,

@ -0,0 +1,31 @@
package manifests_test
import (
"testing"
lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1"
"github.com/ViaQ/loki-operator/internal/manifests"
"github.com/stretchr/testify/require"
)
func TestNewQuerierStatefulSet_SelectorMatchesLabels(t *testing.T) {
// You must set the .spec.selector field of a StatefulSet to match the labels of
// its .spec.template.metadata.labels. Prior to Kubernetes 1.8, the
// .spec.selector field was defaulted when omitted. In 1.8 and later versions,
// failing to specify a matching Pod Selector will result in a validation error
// during StatefulSet creation.
// See https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-selector
ss := manifests.NewQuerierStatefulSet(manifests.Options{
Name: "abcd",
Namespace: "efgh",
Stack: lokiv1beta1.LokiStackSpec{
StorageClassName: "standard",
},
})
l := ss.Spec.Template.GetObjectMeta().GetLabels()
for key, value := range ss.Spec.Selector.MatchLabels {
require.Contains(t, l, key)
require.Equal(t, l[key], value)
}
}

@ -5,8 +5,8 @@ import (
"path"
"github.com/ViaQ/loki-operator/internal/manifests/internal/config"
apps "k8s.io/api/apps/v1"
core "k8s.io/api/core/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/intstr"
@ -24,14 +24,14 @@ func BuildQueryFrontend(stackName string) []client.Object {
}
// NewQueryFrontendDeployment creates a deployment object for a query-frontend
func NewQueryFrontendDeployment(stackName string) *apps.Deployment {
podSpec := core.PodSpec{
Volumes: []core.Volume{
func NewQueryFrontendDeployment(stackName string) *appsv1.Deployment {
podSpec := corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: configVolumeName,
VolumeSource: core.VolumeSource{
ConfigMap: &core.ConfigMapVolumeSource{
LocalObjectReference: core.LocalObjectReference{
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: lokiConfigMapName(stackName),
},
},
@ -39,12 +39,12 @@ func NewQueryFrontendDeployment(stackName string) *apps.Deployment {
},
{
Name: storageVolumeName,
VolumeSource: core.VolumeSource{
EmptyDir: &core.EmptyDirVolumeSource{},
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
},
Containers: []core.Container{
Containers: []corev1.Container{
{
Image: containerImage,
Name: "loki-query-frontend",
@ -52,30 +52,30 @@ func NewQueryFrontendDeployment(stackName string) *apps.Deployment {
"-target=query-frontend",
fmt.Sprintf("-config.file=%s", path.Join(config.LokiConfigMountDir, config.LokiConfigFileName)),
},
ReadinessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
ReadinessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/metrics",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
InitialDelaySeconds: 15,
TimeoutSeconds: 1,
},
LivenessProbe: &core.Probe{
Handler: core.Handler{
HTTPGet: &core.HTTPGetAction{
LivenessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/metrics",
Port: intstr.FromInt(httpPort),
Scheme: core.URISchemeHTTP,
Scheme: corev1.URISchemeHTTP,
},
},
TimeoutSeconds: 2,
PeriodSeconds: 30,
FailureThreshold: 10,
},
Ports: []core.ContainerPort{
Ports: []corev1.ContainerPort{
{
Name: "metrics",
ContainerPort: httpPort,
@ -85,17 +85,17 @@ func NewQueryFrontendDeployment(stackName string) *apps.Deployment {
ContainerPort: grpcPort,
},
},
// Resources: core.ResourceRequirements{
// Limits: core.ResourceList{
// core.ResourceMemory: resource.MustParse("1Gi"),
// core.ResourceCPU: resource.MustParse("1000m"),
// Resources: corev1.ResourceRequirements{
// Limits: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("1Gi"),
// corev1.ResourceCPU: resource.MustParse("1000m"),
// },
// Requests: core.ResourceList{
// core.ResourceMemory: resource.MustParse("50m"),
// core.ResourceCPU: resource.MustParse("50m"),
// Requests: corev1.ResourceList{
// corev1.ResourceMemory: resource.MustParse("50m"),
// corev1.ResourceCPU: resource.MustParse("50m"),
// },
// },
VolumeMounts: []core.VolumeMount{
VolumeMounts: []corev1.VolumeMount{
{
Name: configVolumeName,
ReadOnly: false,
@ -113,49 +113,49 @@ func NewQueryFrontendDeployment(stackName string) *apps.Deployment {
l := ComponentLabels("query-frontend", stackName)
return &apps.Deployment{
return &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-query-frontend-%s", stackName),
Labels: l,
},
Spec: apps.DeploymentSpec{
Spec: appsv1.DeploymentSpec{
Replicas: pointer.Int32Ptr(int32(3)),
Selector: &metav1.LabelSelector{
MatchLabels: labels.Merge(l, GossipLabels()),
},
Template: core.PodTemplateSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-query-frontend-%s", stackName),
Labels: labels.Merge(l, GossipLabels()),
},
Spec: podSpec,
},
Strategy: apps.DeploymentStrategy{
Type: apps.RollingUpdateDeploymentStrategyType,
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
},
},
}
}
// NewQueryFrontendGRPCService creates a k8s service for the querier GRPC endpoint
func NewQueryFrontendGRPCService(stackName string) *core.Service {
func NewQueryFrontendGRPCService(stackName string) *corev1.Service {
l := ComponentLabels("query-frontend", stackName)
return &core.Service{
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-query-frontend-grpc-%s", stackName),
Labels: l,
},
Spec: core.ServiceSpec{
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Ports: []core.ServicePort{
Ports: []corev1.ServicePort{
{
Name: "grpc",
Port: grpcPort,
@ -167,20 +167,20 @@ func NewQueryFrontendGRPCService(stackName string) *core.Service {
}
// NewQueryFrontendHTTPService creates a k8s service for the querier HTTP endpoint
func NewQueryFrontendHTTPService(stackName string) *core.Service {
func NewQueryFrontendHTTPService(stackName string) *corev1.Service {
l := ComponentLabels("query-frontend", stackName)
return &core.Service{
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: apps.SchemeGroupVersion.String(),
APIVersion: corev1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-query-frontend-http-%s", stackName),
Labels: l,
},
Spec: core.ServiceSpec{
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Ports: []core.ServicePort{
Ports: []corev1.ServicePort{
{
Name: "http",
Port: httpPort,

@ -0,0 +1 @@
.DS_Store

@ -0,0 +1,20 @@
language: go
go: 1.13
branches:
only:
- master
env:
global:
# CODECOV_TOKEN
- secure: "O4Hay0TPFW2eq1+Y9vlW4W8VIY9QlKzGgEoL8mgbHQ67RrvqaB7Ft6kdmUaVztu9a5ZB4QwwU8dCFV6D/+dY710qqayQghY2r4igqExzkMyRa3InQtGjhYd9CVpjbR5cXYV1PL9h/Jf3PTTUCmSJp0tpqZCce4BezgnhdSjccGzx3wlzj66+bDdpCzRkWtCvYvI4Fxg5aM5kBke1Ti+aLdEQNzDpkbM38/iyUQDuM+y6ZO+AuP9zqdMY82O6yEMXJppPtVnfqhTJQyDiEF6J6h3TauOb9nKhu8Eg0d2b0HseTRVcCigH3usCm6WYmqhInmsbdAge9gSs9SdYq06VYghG/AvnuRNbmGn3DHuRelyH0gBuXrzZxNP9nfPego4bvk6jQvPSu/flB7JHixkDkFBCO1b2R3ZdOdT6fg+qPQBMxEGy3YJ6ylZ0oqC0AByC+0OP7Hc/xv5U4uqnJ5oBVt4yt26sEjlMs8piXnkoNDoXVjgjfpqLLMyuSRzL1nbf7P270E7L0hDzWInObWlM7FuAl6ghb4j20jvx+4C5UKb8JJPMdbEOcDWZuJOnpThpcNIeP08Xks1wDJ5R01cxK2gja8Hmg+nF320bHybn3RRyOke7tuC3Egez2QaKEoQl9YmcepO+TENcsFk86v8Le68UHi8mJps5mm7iuJb2xyQ="
before_install:
- go get -u golang.org/x/lint/golint
script:
- make ci-test
after_success:
- bash <(curl -s https://codecov.io/bash)

@ -0,0 +1,22 @@
Copyright (c) 2017-present Yuki Iwanaga
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,30 @@
SHELL := /bin/bash -eu -o pipefail
GO_TEST_FLAGS := -v
PACKAGE_DIRS := $(shell go list ./... 2> /dev/null | grep -v /vendor/)
SRC_FILES := $(shell find . -name '*.go' -not -path './vendor/*')
# Tasks
#-----------------------------------------------
.PHONY: lint
lint:
@gofmt -e -d -s $(SRC_FILES) | awk '{ e = 1; print $0 } END { if (e) exit(1) }'
@echo $(SRC_FILES) | xargs -n1 golint -set_exit_status
@go vet $(PACKAGE_DIRS)
.PHONY: test
test: lint
@go test $(GO_TEST_FLAGS) $(PACKAGE_DIRS)
.PHONY: ci-test
ci-test: lint
@echo > coverage.txt
@for d in $(PACKAGE_DIRS); do \
go test -coverprofile=profile.out -covermode=atomic -race -v $$d; \
if [ -f profile.out ]; then \
cat profile.out >> coverage.txt; \
rm profile.out; \
fi; \
done

@ -0,0 +1,69 @@
defaults
========
[![Build Status](https://travis-ci.org/creasty/defaults.svg?branch=master)](https://travis-ci.org/creasty/defaults)
[![codecov](https://codecov.io/gh/creasty/defaults/branch/master/graph/badge.svg)](https://codecov.io/gh/creasty/defaults)
[![GitHub release](https://img.shields.io/github/release/creasty/defaults.svg)](https://github.com/creasty/defaults/releases)
[![License](https://img.shields.io/github/license/creasty/defaults.svg)](./LICENSE)
Initialize structs with default values
- Supports almost all kind of types
- Scalar types
- `int/8/16/32/64`, `uint/8/16/32/64`, `float32/64`
- `uintptr`, `bool`, `string`
- Complex types
- `map`, `slice`, `struct`
- Aliased types
- `time.Duration`
- e.g., `type Enum string`
- Pointer types
- e.g., `*SampleStruct`, `*int`
- Recursively initializes fields in a struct
- Dynamically sets default values by [`defaults.Setter`](./setter.go) interface
- Preserves non-initial values from being reset with a default value
Usage
-----
```go
type Gender string
type Sample struct {
Name string `default:"John Smith"`
Age int `default:"27"`
Gender Gender `default:"m"`
Slice []string `default:"[]"`
SliceByJSON []int `default:"[1, 2, 3]"` // Supports JSON
Map map[string]int `default:"{}"`
MapByJSON map[string]int `default:"{\"foo\": 123}"`
Struct OtherStruct `default:"{}"`
StructPtr *OtherStruct `default:"{\"Foo\": 123}"`
NoTag OtherStruct // Recurses into a nested struct by default
OptOut OtherStruct `default:"-"` // Opt-out
}
type OtherStruct struct {
Hello string `default:"world"` // Tags in a nested struct also work
Foo int `default:"-"`
Random int `default:"-"`
}
// SetDefaults implements defaults.Setter interface
func (s *OtherStruct) SetDefaults() {
if defaults.CanUpdate(s.Random) { // Check if it's a zero value (recommended)
s.Random = rand.Int() // Set a dynamic value
}
}
```
```go
obj := &Sample{}
if err := defaults.Set(obj); err != nil {
panic(err)
}
```

@ -0,0 +1,195 @@
package defaults
import (
"encoding/json"
"errors"
"reflect"
"strconv"
"time"
)
var (
errInvalidType = errors.New("not a struct pointer")
)
const (
fieldName = "default"
)
// Set initializes members in a struct referenced by a pointer.
// Maps and slices are initialized by `make` and other primitive types are set with default values.
// `ptr` should be a struct pointer
func Set(ptr interface{}) error {
if reflect.TypeOf(ptr).Kind() != reflect.Ptr {
return errInvalidType
}
v := reflect.ValueOf(ptr).Elem()
t := v.Type()
if t.Kind() != reflect.Struct {
return errInvalidType
}
for i := 0; i < t.NumField(); i++ {
if defaultVal := t.Field(i).Tag.Get(fieldName); defaultVal != "-" {
if err := setField(v.Field(i), defaultVal); err != nil {
return err
}
}
}
callSetter(ptr)
return nil
}
// MustSet function is a wrapper of Set function
// It will call Set and panic if err not equals nil.
func MustSet(ptr interface{}) {
if err := Set(ptr); err != nil {
panic(err)
}
}
func setField(field reflect.Value, defaultVal string) error {
if !field.CanSet() {
return nil
}
if !shouldInitializeField(field, defaultVal) {
return nil
}
if isInitialValue(field) {
switch field.Kind() {
case reflect.Bool:
if val, err := strconv.ParseBool(defaultVal); err == nil {
field.Set(reflect.ValueOf(val).Convert(field.Type()))
}
case reflect.Int:
if val, err := strconv.ParseInt(defaultVal, 0, strconv.IntSize); err == nil {
field.Set(reflect.ValueOf(int(val)).Convert(field.Type()))
}
case reflect.Int8:
if val, err := strconv.ParseInt(defaultVal, 0, 8); err == nil {
field.Set(reflect.ValueOf(int8(val)).Convert(field.Type()))
}
case reflect.Int16:
if val, err := strconv.ParseInt(defaultVal, 0, 16); err == nil {
field.Set(reflect.ValueOf(int16(val)).Convert(field.Type()))
}
case reflect.Int32:
if val, err := strconv.ParseInt(defaultVal, 0, 32); err == nil {
field.Set(reflect.ValueOf(int32(val)).Convert(field.Type()))
}
case reflect.Int64:
if val, err := time.ParseDuration(defaultVal); err == nil {
field.Set(reflect.ValueOf(val).Convert(field.Type()))
} else if val, err := strconv.ParseInt(defaultVal, 0, 64); err == nil {
field.Set(reflect.ValueOf(val).Convert(field.Type()))
}
case reflect.Uint:
if val, err := strconv.ParseUint(defaultVal, 0, strconv.IntSize); err == nil {
field.Set(reflect.ValueOf(uint(val)).Convert(field.Type()))
}
case reflect.Uint8:
if val, err := strconv.ParseUint(defaultVal, 0, 8); err == nil {
field.Set(reflect.ValueOf(uint8(val)).Convert(field.Type()))
}
case reflect.Uint16:
if val, err := strconv.ParseUint(defaultVal, 0, 16); err == nil {
field.Set(reflect.ValueOf(uint16(val)).Convert(field.Type()))
}
case reflect.Uint32:
if val, err := strconv.ParseUint(defaultVal, 0, 32); err == nil {
field.Set(reflect.ValueOf(uint32(val)).Convert(field.Type()))
}
case reflect.Uint64:
if val, err := strconv.ParseUint(defaultVal, 0, 64); err == nil {
field.Set(reflect.ValueOf(val).Convert(field.Type()))
}
case reflect.Uintptr:
if val, err := strconv.ParseUint(defaultVal, 0, strconv.IntSize); err == nil {
field.Set(reflect.ValueOf(uintptr(val)).Convert(field.Type()))
}
case reflect.Float32:
if val, err := strconv.ParseFloat(defaultVal, 32); err == nil {
field.Set(reflect.ValueOf(float32(val)).Convert(field.Type()))
}
case reflect.Float64:
if val, err := strconv.ParseFloat(defaultVal, 64); err == nil {
field.Set(reflect.ValueOf(val).Convert(field.Type()))
}
case reflect.String:
field.Set(reflect.ValueOf(defaultVal).Convert(field.Type()))
case reflect.Slice:
ref := reflect.New(field.Type())
ref.Elem().Set(reflect.MakeSlice(field.Type(), 0, 0))
if defaultVal != "" && defaultVal != "[]" {
if err := json.Unmarshal([]byte(defaultVal), ref.Interface()); err != nil {
return err
}
}
field.Set(ref.Elem().Convert(field.Type()))
case reflect.Map:
ref := reflect.New(field.Type())
ref.Elem().Set(reflect.MakeMap(field.Type()))
if defaultVal != "" && defaultVal != "{}" {
if err := json.Unmarshal([]byte(defaultVal), ref.Interface()); err != nil {
return err
}
}
field.Set(ref.Elem().Convert(field.Type()))
case reflect.Struct:
if defaultVal != "" && defaultVal != "{}" {
if err := json.Unmarshal([]byte(defaultVal), field.Addr().Interface()); err != nil {
return err
}
}
case reflect.Ptr:
field.Set(reflect.New(field.Type().Elem()))
}
}
switch field.Kind() {
case reflect.Ptr:
setField(field.Elem(), defaultVal)
callSetter(field.Interface())
case reflect.Struct:
if err := Set(field.Addr().Interface()); err != nil {
return err
}
case reflect.Slice:
for j := 0; j < field.Len(); j++ {
if err := setField(field.Index(j), defaultVal); err != nil {
return err
}
}
}
return nil
}
func isInitialValue(field reflect.Value) bool {
return reflect.DeepEqual(reflect.Zero(field.Type()).Interface(), field.Interface())
}
func shouldInitializeField(field reflect.Value, tag string) bool {
switch field.Kind() {
case reflect.Struct:
return true
case reflect.Ptr:
if !field.IsNil() && field.Elem().Kind() == reflect.Struct {
return true
}
case reflect.Slice:
return field.Len() > 0 || tag != ""
}
return tag != ""
}
// CanUpdate returns true when the given value is an initial value of its type
func CanUpdate(v interface{}) bool {
return isInitialValue(reflect.ValueOf(v))
}

@ -0,0 +1,3 @@
module github.com/creasty/defaults
go 1.13

@ -0,0 +1,12 @@
package defaults
// Setter is an interface for setting default values
type Setter interface {
SetDefaults()
}
func callSetter(v interface{}) {
if ds, ok := v.(Setter); ok {
ds.SetDefaults()
}
}

@ -22,6 +22,9 @@ github.com/ViaQ/logerr/log
github.com/beorn7/perks/quantile
# github.com/cespare/xxhash/v2 v2.1.1
github.com/cespare/xxhash/v2
# github.com/creasty/defaults v1.5.1
## explicit
github.com/creasty/defaults
# github.com/davecgh/go-spew v1.1.1
github.com/davecgh/go-spew/spew
# github.com/evanphx/json-patch v4.9.0+incompatible
@ -484,4 +487,5 @@ sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics
# sigs.k8s.io/structured-merge-diff/v4 v4.0.2
sigs.k8s.io/structured-merge-diff/v4/value
# sigs.k8s.io/yaml v1.2.0
## explicit
sigs.k8s.io/yaml

Loading…
Cancel
Save