WIP: first commit
This commit is contained in:
		
							parent
							
								
									9cb48e0889
								
							
						
					
					
						commit
						5a1b944fed
					
				|  | @ -155,7 +155,7 @@ func (c *Cluster) setStatus(status spec.PostgresStatus) { | |||
| 
 | ||||
| 	_, err = c.KubeClient.CRDREST.Patch(types.MergePatchType). | ||||
| 		Namespace(c.Namespace). | ||||
| 		Resource(constants.CRDResource). | ||||
| 		Resource(constants.PostgresCRDResource). | ||||
| 		Name(c.Name). | ||||
| 		Body(request). | ||||
| 		DoRaw() | ||||
|  |  | |||
|  | @ -424,7 +424,7 @@ func (c *Cluster) credentialSecretNameForCluster(username string, clusterName st | |||
| 	return c.OpConfig.SecretNameTemplate.Format( | ||||
| 		"username", strings.Replace(username, "_", "-", -1), | ||||
| 		"cluster", clusterName, | ||||
| 		"tprkind", constants.CRDKind, | ||||
| 		"tprkind", constants.PostgresCRDKind, | ||||
| 		"tprgroup", constants.CRDGroup) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -174,8 +174,21 @@ func (c *Controller) initController() { | |||
| 		c.logger.Logger.Level = logrus.DebugLevel | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.createCRD(); err != nil { | ||||
| 		c.logger.Fatalf("could not register CustomResourceDefinition: %v", err) | ||||
| 
 | ||||
| 	if err := c.createPostgresCRD(); err != nil { | ||||
| 		c.logger.Fatalf("could not register Postgres CustomResourceDefinition: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.createOperatorCRD(); err != nil { | ||||
| 		c.logger.Fatalf("could not register Operator Configuration CustomResourceDefinition: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if configObjectName := os.Getenv("POSTGRES_OPERATOR_CONFIGURATION_OBJECT"); configObjectName != "" { | ||||
| 		if config, err := c.readOperatorConfigurationFromCRD(configObjectName); err != nil { | ||||
| 			c.logger.Fatalf("unable to read operator configuration: %v", err) | ||||
| 		} else { | ||||
| 			c.logger.Fatalf("operator configuration: %#v", config) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if infraRoles, err := c.getInfrastructureRoles(&c.opConfig.InfrastructureRolesSecretName); err != nil { | ||||
|  |  | |||
|  | @ -0,0 +1,34 @@ | |||
| package controller | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"github.com/zalando-incubator/postgres-operator/pkg/util/constants" | ||||
| 
 | ||||
| 	"github.com/zalando-incubator/postgres-operator/pkg/util/config" | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| func (c *Controller) readOperatorConfigurationFromCRD(configObjectName string) (*config.OperatorConfiguration, error){ | ||||
| 	var ( | ||||
| 		config config.OperatorConfiguration | ||||
| 	) | ||||
| 
 | ||||
| 	req := c.KubeClient.CRDREST.Get(). | ||||
| 		Name(configObjectName). | ||||
| 		Namespace(c.opConfig.WatchedNamespace). | ||||
| 		Resource(constants.OperatorConfigCRDResource). | ||||
| 		VersionedParams(&metav1.ListOptions{ResourceVersion: "0"}, metav1.ParameterCodec) | ||||
| 
 | ||||
| 	data, err := req.DoRaw(); | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not get operator configuration object %s: %v", configObjectName, err) | ||||
| 	} | ||||
| 	if err = json.Unmarshal(data, &config); err != nil { | ||||
| 		return nil, fmt.Errorf("could not unmarshal operator configuration object %s, %v", configObjectName, err) | ||||
| 	} | ||||
| 
 | ||||
| 	return &config, nil | ||||
| } | ||||
|  | @ -48,7 +48,7 @@ func (c *Controller) clusterListFunc(options metav1.ListOptions) (runtime.Object | |||
| 	req := c.KubeClient.CRDREST. | ||||
| 		Get(). | ||||
| 		Namespace(c.opConfig.WatchedNamespace). | ||||
| 		Resource(constants.CRDResource). | ||||
| 		Resource(constants.PostgresCRDResource). | ||||
| 		VersionedParams(&options, metav1.ParameterCodec) | ||||
| 
 | ||||
| 	b, err := req.DoRaw() | ||||
|  | @ -117,7 +117,7 @@ func (c *Controller) clusterWatchFunc(options metav1.ListOptions) (watch.Interfa | |||
| 	r, err := c.KubeClient.CRDREST. | ||||
| 		Get(). | ||||
| 		Namespace(c.opConfig.WatchedNamespace). | ||||
| 		Resource(constants.CRDResource). | ||||
| 		Resource(constants.PostgresCRDResource). | ||||
| 		VersionedParams(&options, metav1.ParameterCodec). | ||||
| 		FieldsSelectorParam(nil). | ||||
| 		Stream() | ||||
|  |  | |||
|  | @ -47,20 +47,20 @@ func (c *Controller) clusterWorkerID(clusterName spec.NamespacedName) uint32 { | |||
| 	return c.clusterWorkers[clusterName] | ||||
| } | ||||
| 
 | ||||
| func (c *Controller) createCRD() error { | ||||
| func (c *Controller) createZalandoCRD(plural, singular, short string) error { | ||||
| 	crd := &apiextv1beta1.CustomResourceDefinition{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: constants.CRDResource + "." + constants.CRDGroup, | ||||
| 			Name: plural + "." + constants.CRDGroup, | ||||
| 		}, | ||||
| 		Spec: apiextv1beta1.CustomResourceDefinitionSpec{ | ||||
| 			Group:   constants.CRDGroup, | ||||
| 			Version: constants.CRDApiVersion, | ||||
| 			Names: apiextv1beta1.CustomResourceDefinitionNames{ | ||||
| 				Plural:     constants.CRDResource, | ||||
| 				Singular:   constants.CRDKind, | ||||
| 				ShortNames: []string{constants.CRDShort}, | ||||
| 				Kind:       constants.CRDKind, | ||||
| 				ListKind:   constants.CRDKind + "List", | ||||
| 				Plural:     plural, | ||||
| 				Singular:   singular, | ||||
| 				ShortNames: []string{short}, | ||||
| 				Kind:       singular, | ||||
| 				ListKind:   singular + "List", | ||||
| 			}, | ||||
| 			Scope: apiextv1beta1.NamespaceScoped, | ||||
| 		}, | ||||
|  | @ -98,6 +98,14 @@ func (c *Controller) createCRD() error { | |||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (c *Controller) createPostgresCRD() error { | ||||
| 	return c.createZalandoCRD(constants.PostgresCRDResource, constants.PostgresCRDKind, constants.PostgresCRDShort) | ||||
| } | ||||
| 
 | ||||
| func (c *Controller) createOperatorCRD() error { | ||||
| 	return c.createZalandoCRD(constants.OperatorConfigCRDResource, constants.OperatorConfigCRDKind, constants.OperatorConfigCRDShort) | ||||
| } | ||||
| 
 | ||||
| func readDecodedRole(s string) (*spec.PgUser, error) { | ||||
| 	var result spec.PgUser | ||||
| 	if err := yaml.Unmarshal([]byte(s), &result); err != nil { | ||||
|  |  | |||
|  | @ -140,7 +140,7 @@ var ( | |||
| ) | ||||
| 
 | ||||
| // Clone makes a deepcopy of the Postgresql structure. The Error field is nulled-out,
 | ||||
| // as there is no guaratee that the actual implementation of the error interface
 | ||||
| // as there is no guarantee that the actual implementation of the error interface
 | ||||
| // will not contain any private fields not-reachable to deepcopy. This should be ok,
 | ||||
| // since Error is never read from a Kubernetes object.
 | ||||
| func (p *Postgresql) Clone() *Postgresql { | ||||
|  |  | |||
|  | @ -40,11 +40,11 @@ type Resources struct { | |||
| 
 | ||||
| // Auth describes authentication specific configuration parameters
 | ||||
| type Auth struct { | ||||
| 	SecretNameTemplate            stringTemplate      `name:"secret_name_template" default:"{username}.{cluster}.credentials.{tprkind}.{tprgroup}"` | ||||
| 	PamRoleName                   string              `name:"pam_role_name" default:"zalandos"` | ||||
| 	PamConfiguration              string              `name:"pam_configuration" default:"https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"` | ||||
| 	TeamsAPIUrl                   string              `name:"teams_api_url" default:"https://teams.example.com/api/"` | ||||
| 	OAuthTokenSecretName          spec.NamespacedName `name:"oauth_token_secret_name" default:"postgresql-operator"` | ||||
| 	SecretNameTemplate   StringTemplate      `name:"secret_name_template" default:"{username}.{cluster}.credentials.{tprkind}.{tprgroup}"` | ||||
| 	PamRoleName          string              `name:"pam_role_name" default:"zalandos"` | ||||
| 	PamConfiguration     string              `name:"pam_configuration" default:"https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"` | ||||
| 	TeamsAPIUrl          string              `name:"teams_api_url" default:"https://teams.example.com/api/"` | ||||
| 	OAuthTokenSecretName spec.NamespacedName `name:"oauth_token_secret_name" default:"postgresql-operator"` | ||||
| 	InfrastructureRolesSecretName spec.NamespacedName `name:"infrastructure_roles_secret_name"` | ||||
| 	SuperUsername                 string              `name:"super_username" default:"postgres"` | ||||
| 	ReplicationUsername           string              `name:"replication_username" default:"standby"` | ||||
|  | @ -88,9 +88,9 @@ type Config struct { | |||
| 	EnableReplicaLoadBalancer   bool   `name:"enable_replica_load_balancer" default:"false"` | ||||
| 	// deprecated and kept for backward compatibility
 | ||||
| 	EnableLoadBalancer       *bool             `name:"enable_load_balancer"` | ||||
| 	MasterDNSNameFormat      stringTemplate    `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"` | ||||
| 	ReplicaDNSNameFormat     stringTemplate    `name:"replica_dns_name_format" default:"{cluster}-repl.{team}.{hostedzone}"` | ||||
| 	PDBNameFormat            stringTemplate    `name:"pdb_name_format" default:"postgres-{cluster}-pdb"` | ||||
| 	MasterDNSNameFormat      StringTemplate    `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"` | ||||
| 	ReplicaDNSNameFormat     StringTemplate    `name:"replica_dns_name_format" default:"{cluster}-repl.{team}.{hostedzone}"` | ||||
| 	PDBNameFormat            StringTemplate    `name:"pdb_name_format" default:"postgres-{cluster}-pdb"` | ||||
| 	Workers                  uint32            `name:"workers" default:"4"` | ||||
| 	APIPort                  int               `name:"api_port" default:"8080"` | ||||
| 	RingLogLines             int               `name:"ring_log_lines" default:"100"` | ||||
|  |  | |||
|  | @ -0,0 +1,165 @@ | |||
| package config | ||||
| 
 | ||||
| import ( | ||||
| 
 | ||||
| 	"encoding/json" | ||||
| 	"time" | ||||
| 
 | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 
 | ||||
| 	"github.com/zalando-incubator/postgres-operator/pkg/spec" | ||||
| ) | ||||
| 
 | ||||
| type OperatorConfiguration struct { | ||||
| 	metav1.TypeMeta   `json:",inline"` | ||||
| 	metav1.ObjectMeta `json:"metadata"` | ||||
| 
 | ||||
| 	Configuration OperatorConfigurationData `json:"configuration"` | ||||
| 	Error         error                      `json:"-"` | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| type OperatorConfigurationList struct { | ||||
| 	metav1.TypeMeta   `json:",inline"` | ||||
| 	metav1.ListMeta `json:"metadata"` | ||||
| 
 | ||||
| 	Items []OperatorConfiguration `json:"items"` | ||||
| } | ||||
| 
 | ||||
| type PostgresUsersConfiguration struct { | ||||
| 	SuperUsername                 string              `json:"super_username,omitempty"` | ||||
| 	ReplicationUsername           string              `json:"replication_username,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type KubernetesMetaConfiguration struct { | ||||
| 	PodServiceAccountName string `json:"pod_service_account_name,omitempty"` | ||||
| 	// TODO: change it to the proper json
 | ||||
| 	PodServiceAccountDefinition   string                `json:"pod_service_account_definition,omitempty"` | ||||
| 	PodTerminateGracePeriod       time.Duration         `json:"pod_terminate_grace_period,omitempty"` | ||||
| 	WatchedNamespace              string                `json:"watched_namespace,omitempty"` | ||||
| 	PDBNameFormat                 StringTemplate `json:"pdb_name_format,omitempty"` | ||||
| 	SecretNameTemplate            StringTemplate `json:"secret_name_template,omitempty"` | ||||
| 	OAuthTokenSecretName          spec.NamespacedName   `json:"oauth_token_secret_name,omitempty"` | ||||
| 	InfrastructureRolesSecretName spec.NamespacedName   `json:"infrastructure_roles_secret_name,omitempty"` | ||||
| 	PodRoleLabel                  string                `json:"pod_role_label,omitempty"` | ||||
| 	ClusterLabels                 map[string]string     `json:"cluster_labels,omitempty"` | ||||
| 	ClusterNameLabel              string                `json:"cluster_name_label,omitempty"` | ||||
| 	NodeReadinessLabel            map[string]string     `json:"node_readiness_label,omitempty"` | ||||
| 	// TODO: use a proper toleration structure?
 | ||||
| 	PodToleration map[string]string `json:"toleration,omitempty"` | ||||
| 	// TODO: use namespacedname
 | ||||
| 	PodEnvironmentConfigMap string `json:"pod_environment_configmap,omitempty"` | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type PostgresPodResourcesDefaults struct { | ||||
| 	DefaultCPURequest       string            `json:"default_cpu_request,omitempty"` | ||||
| 	DefaultMemoryRequest    string            `json:"default_memory_request,omitempty"` | ||||
| 	DefaultCPULimit         string            `json:"default_cpu_limit,omitempty"` | ||||
| 	DefaultMemoryLimit      string            `json:"default_memory_limit,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type OperatorTimeouts struct { | ||||
| 	ResourceCheckInterval   time.Duration     `json:"resource_check_interval,omitempty"` | ||||
| 	ResourceCheckTimeout    time.Duration     `json:"resource_check_timeout,omitempty"` | ||||
| 	PodLabelWaitTimeout     time.Duration     `json:"pod_label_wait_timeout,omitempty"` | ||||
| 	PodDeletionWaitTimeout  time.Duration     `json:"pod_deletion_wait_timeout,omitempty"` | ||||
| 	ReadyWaitInterval time.Duration `json:"ready_wait_interval,omitempty"` | ||||
| 	ReadyWaitTimeout  time.Duration `json:"ready_wait_timeout,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type LoadBalancerConfiguration struct { | ||||
| 	DbHostedZone                string `json:"db_hosted_zone,omitempty"` | ||||
| 	EnableMasterLoadBalancer    bool   `json:"enable_master_load_balancer,omitempty"` | ||||
| 	EnableReplicaLoadBalancer   bool   `json:"enable_replica_load_balancer,omitempty"` | ||||
| 	MasterDNSNameFormat      StringTemplate    `json:"master_dns_name_format,omitempty"` | ||||
| 	ReplicaDNSNameFormat     StringTemplate    `json:"replica_dns_name_format,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type AWSGCPConfiguration struct { | ||||
| 	WALES3Bucket                string `json:"wal_s3_bucket,omitempty"` | ||||
| 	LogS3Bucket                 string `json:"log_s3_bucket,omitempty"` | ||||
| 	KubeIAMRole                 string `json:"kube_iam_role,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type OperatorDebugConfiguration struct { | ||||
| 	DebugLogging                bool   `json:"debug_logging,omitempty"` | ||||
| 	EnableDBAccess              bool   `json:"enable_database_access,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type TeamsAPIConfiguration struct { | ||||
| 	EnableTeamsAPI              bool   `json:"enable_teams_api,omitempty"` | ||||
| 	TeamsAPIUrl          string              `json:"teams_api_url,omitempty"` | ||||
| 	TeamAPIRoleConfiguration map[string]string `json:"team_api_role_configuration,omitempty"` | ||||
| 	EnableTeamSuperuser         bool   `json:"enable_team_superuser,omitempty"` | ||||
| 	TeamAdminRole               string `json:"team_admin_role,omitempty"` | ||||
| 	PamRoleName          string              `json:"pam_role_name,omitempty"` | ||||
| 	PamConfiguration     string              `json:"pam_configuration,omitempty"` | ||||
| 	ProtectedRoles           []string          `json:"protected_role_names,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type LoggingRESTAPIConfiguration struct { | ||||
| 	APIPort                  int               `json:"api_port,omitempty"` | ||||
| 	RingLogLines             int               `json:"ring_log_lines,omitempty"` | ||||
| 	ClusterHistoryEntries    int               `json:"cluster_history_entries,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type ScalyrConfiguration struct { | ||||
| 	ScalyrAPIKey        string `json:"scalyr_api_key,omitempty"` | ||||
| 	ScalyrImage         string `json:"scalyr_image,omitempty"` | ||||
| 	ScalyrServerURL     string `json:"scalyr_server_url,omitempty"` | ||||
| 	ScalyrCPURequest    string `json:"scalyr_cpu_request,omitempty"` | ||||
| 	ScalyrMemoryRequest string `json:"scalyr_memory_request,omitempty"` | ||||
| 	ScalyrCPULimit      string `json:"scalyr_cpu_limit,omitempty"` | ||||
| 	ScalyrMemoryLimit   string `json:"scalyr_memory_limit,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type OperatorConfigurationData struct { | ||||
| 	EtcdHost string `json:"etcd_host,omitempty"` | ||||
| 	DockerImage string `json:"docker_image,omitempty"` | ||||
| 	Workers int `json:"workers,omitempty"` | ||||
| 	MinInstances int32 `json:"min_instances,omitempty"` | ||||
| 	MaxInstances int32 `json:"max_instances,omitempty"` | ||||
| 	ResyncPeriod time.Duration `json:"resync_period,omitempty"` | ||||
| 	PostgresUsersConfiguration PostgresUsersConfiguration `json:"users"` | ||||
| 	Kubernetes KubernetesMetaConfiguration `json:"kubernetes"` | ||||
| 	PostgresPodResources PostgresPodResourcesDefaults `json:"postgres_pod_resources"` | ||||
| 	Timeouts OperatorTimeouts `json:"timeouts"` | ||||
| 	LoadBalancer LoadBalancerConfiguration `json:"load_balancer"` | ||||
| 	AWSGCP AWSGCPConfiguration `json:"aws_or_gcp"` | ||||
| 	OperatorDebug OperatorDebugConfiguration `json:"debug"` | ||||
| 	TeamsAPI TeamsAPIConfiguration `json:"teams_api"` | ||||
| 	LoggingRESTAPI LoggingRESTAPIConfiguration `json:"logging_rest_api"` | ||||
| 	Scalyr ScalyrConfiguration `json:"scalyr"` | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| type OperatorConfigurationUsers struct { | ||||
| 	SuperUserName string `json:"superuser_name,omitempty"` | ||||
| 	Replication string `json:"replication_user_name,omitempty"` | ||||
| 	ProtectedRoles []string `json:"protected_roles,omitempty"` | ||||
| 	TeamAPIRoleConfiguration map[string]string `json:"team_api_role_configuration,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type OperatorConfigurationCopy OperatorConfiguration | ||||
| type OperatorConfigurationListCopy OperatorConfigurationList | ||||
| 
 | ||||
| 
 | ||||
| func (opc *OperatorConfiguration) UnmarshalJSON(data []byte) error { | ||||
| 	var ref OperatorConfigurationCopy | ||||
| 	if err := json.Unmarshal(data, &ref); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*opc = OperatorConfiguration(ref) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (opcl *OperatorConfigurationList) UnmarshalJSON(data []byte) error { | ||||
| 	var ref OperatorConfigurationListCopy | ||||
| 	if err := json.Unmarshal(data, &ref); err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	*opcl = OperatorConfigurationList(ref) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  | @ -19,7 +19,7 @@ type fieldInfo struct { | |||
| 	Field   reflect.Value | ||||
| } | ||||
| 
 | ||||
| type stringTemplate string | ||||
| type StringTemplate string | ||||
| 
 | ||||
| func decoderFrom(field reflect.Value) (d decoder) { | ||||
| 	// it may be impossible for a struct field to fail this check
 | ||||
|  | @ -222,13 +222,13 @@ func getMapPairsFromString(value string) (pairs []string, err error) { | |||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (f *stringTemplate) Decode(value string) error { | ||||
| 	*f = stringTemplate(value) | ||||
| func (f *StringTemplate) Decode(value string) error { | ||||
| 	*f = StringTemplate(value) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (f *stringTemplate) Format(a ...string) string { | ||||
| func (f *StringTemplate) Format(a ...string) string { | ||||
| 	res := string(*f) | ||||
| 
 | ||||
| 	for i := 0; i < len(a); i += 2 { | ||||
|  | @ -238,6 +238,6 @@ func (f *stringTemplate) Format(a ...string) string { | |||
| 	return res | ||||
| } | ||||
| 
 | ||||
| func (f stringTemplate) MarshalJSON() ([]byte, error) { | ||||
| func (f StringTemplate) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(string(f)) | ||||
| } | ||||
|  |  | |||
|  | @ -2,9 +2,13 @@ package constants | |||
| 
 | ||||
| // Different properties of the PostgreSQL Custom Resource Definition
 | ||||
| const ( | ||||
| 	CRDKind       = "postgresql" | ||||
| 	CRDResource   = "postgresqls" | ||||
| 	CRDShort      = "pg" | ||||
| 	CRDGroup      = "acid.zalan.do" | ||||
| 	CRDApiVersion = "v1" | ||||
| 	PostgresCRDKind     = "postgresql" | ||||
| 	PostgresCRDResource = "postgresqls" | ||||
| 	PostgresCRDShort    = "pg" | ||||
| 	CRDGroup            = "acid.zalan.do" | ||||
| 	CRDApiVersion       = "v1" | ||||
| 	OperatorConfigCRDKind = "postgresql-operator-configuration" | ||||
| 	OperatorConfigCRDResource = "postgresql-operator-configurations" | ||||
| 	OperatorConfigCRDShort = "pgopconfig" | ||||
| 
 | ||||
| ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue