mirror of
https://github.com/helm/helm.git
synced 2026-05-28 04:35:48 -04:00
feat(tiller): add template and release to install
This commit is contained in:
parent
d3cff27bf9
commit
50f4349512
11 changed files with 118 additions and 81 deletions
|
|
@ -18,7 +18,7 @@ message Chart {
|
|||
hapi.chart.Metadata metadata = 1;
|
||||
|
||||
// Templates for this chart.
|
||||
hapi.chart.Templates templates = 2;
|
||||
repeated hapi.chart.Template templates = 2;
|
||||
|
||||
// Charts that this chart depends on.
|
||||
repeated Chart dependencies = 3;
|
||||
|
|
|
|||
|
|
@ -4,20 +4,14 @@ package hapi.chart;
|
|||
|
||||
option go_package = "chart";
|
||||
|
||||
// Template represents a template as a name/value pair.
|
||||
//
|
||||
// Template:
|
||||
//
|
||||
// TODO
|
||||
//
|
||||
message Templates {
|
||||
// TODO
|
||||
repeated Template templates = 1;
|
||||
}
|
||||
|
||||
// By convention, name is a relative path within the scope of the chart's
|
||||
// base directory.
|
||||
message Template {
|
||||
// TODO
|
||||
string template_name = 1;
|
||||
|
||||
// TODO
|
||||
bytes template_data = 2;
|
||||
// Name is the path-like name of the template.
|
||||
string name = 1;
|
||||
|
||||
// Data is the template as byte data.
|
||||
bytes data = 2;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package environment
|
|||
import (
|
||||
"github.com/deis/tiller/pkg/engine"
|
||||
"github.com/deis/tiller/pkg/hapi"
|
||||
"github.com/deis/tiller/pkg/proto/hapi/chart"
|
||||
"github.com/deis/tiller/pkg/storage"
|
||||
)
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ func (y EngineYard) Default() Engine {
|
|||
// An Engine must be capable of executing multiple concurrent requests, but
|
||||
// without tainting one request's environment with data from another request.
|
||||
type Engine interface {
|
||||
Render(*hapi.Chart, *hapi.Values) (map[string]string, error)
|
||||
Render(*chart.Chart, *chart.Config) (map[string]string, error)
|
||||
}
|
||||
|
||||
// ReleaseStorage represents a storage engine for a Release.
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/deis/tiller/pkg/hapi"
|
||||
"github.com/deis/tiller/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
type mockEngine struct {
|
||||
out map[string]string
|
||||
}
|
||||
|
||||
func (e *mockEngine) Render(chrt *hapi.Chart, v *hapi.Values) (map[string]string, error) {
|
||||
func (e *mockEngine) Render(chrt *chart.Chart, v *chart.Config) (map[string]string, error) {
|
||||
return e.out, nil
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ func TestEngine(t *testing.T) {
|
|||
|
||||
if engine, ok := env.EngineYard.Get("test"); !ok {
|
||||
t.Errorf("failed to get engine from EngineYard")
|
||||
} else if out, err := engine.Render(&hapi.Chart{}, &hapi.Values{}); err != nil {
|
||||
} else if out, err := engine.Render(&chart.Chart{}, &chart.Config{}); err != nil {
|
||||
t.Errorf("unexpected template error: %s", err)
|
||||
} else if out["albatross"] != "test" {
|
||||
t.Errorf("expected 'test', got %q", out["albatross"])
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"errors"
|
||||
|
||||
"github.com/deis/tiller/cmd/tiller/environment"
|
||||
"github.com/deis/tiller/pkg/proto/hapi/release"
|
||||
"github.com/deis/tiller/pkg/proto/hapi/services"
|
||||
"github.com/technosophos/moniker"
|
||||
ctx "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
|
|
@ -19,8 +21,11 @@ type releaseServer struct {
|
|||
env *environment.Environment
|
||||
}
|
||||
|
||||
// errNotImplemented is a temporary error for uninmplemented callbacks.
|
||||
var errNotImplemented = errors.New("not implemented")
|
||||
var (
|
||||
// errNotImplemented is a temporary error for uninmplemented callbacks.
|
||||
errNotImplemented = errors.New("not implemented")
|
||||
errMissingChart = errors.New("no chart provided")
|
||||
)
|
||||
|
||||
func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error {
|
||||
return errNotImplemented
|
||||
|
|
@ -39,7 +44,32 @@ func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease
|
|||
}
|
||||
|
||||
func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) {
|
||||
return &services.InstallReleaseResponse{}, errNotImplemented
|
||||
if req.Chart == nil {
|
||||
return nil, errMissingChart
|
||||
}
|
||||
|
||||
// We should probably make a name generator part of the Environment.
|
||||
namer := moniker.New()
|
||||
// TODO: Make sure this is unique.
|
||||
name := namer.Name()
|
||||
|
||||
// Render the templates
|
||||
_, err := s.env.EngineYard.Default().Render(req.Chart, req.Values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store a release.
|
||||
r := &release.Release{
|
||||
Name: name,
|
||||
Chart: req.Chart,
|
||||
Config: req.Values,
|
||||
Info: &release.Info{
|
||||
Status: &release.Status{Code: release.Status_UNKNOWN},
|
||||
},
|
||||
}
|
||||
|
||||
return &services.InstallReleaseResponse{Release: r}, errNotImplemented
|
||||
}
|
||||
|
||||
func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) {
|
||||
|
|
|
|||
10
glide.lock
generated
10
glide.lock
generated
|
|
@ -1,5 +1,5 @@
|
|||
hash: e7c99013acb06eb359cf20390579af9a4553ef0fbed3f7bbb784b4ab7c8df807
|
||||
updated: 2016-04-15T15:15:21.87772545-06:00
|
||||
hash: 264d156a2a07d53efbf5f608ead3eb31c261de5124e0235139b3f99c6ead4dba
|
||||
updated: 2016-04-18T17:25:07.662942088-06:00
|
||||
imports:
|
||||
- name: github.com/aokoli/goutils
|
||||
version: 9c37978a95bd5c709a15883b6242714ea6709e64
|
||||
|
|
@ -11,6 +11,8 @@ imports:
|
|||
version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c
|
||||
subpackages:
|
||||
- proto
|
||||
- ptypes/any
|
||||
- ptypes/timestamp
|
||||
- name: github.com/Masterminds/semver
|
||||
version: 808ed7761c233af2de3f9729a041d68c62527f3a
|
||||
- name: github.com/Masterminds/sprig
|
||||
|
|
@ -21,6 +23,8 @@ imports:
|
|||
- cobra
|
||||
- name: github.com/spf13/pflag
|
||||
version: 8f6a28b0916586e7f22fe931ae2fcfc380b1c0e6
|
||||
- name: github.com/technosophos/moniker
|
||||
version: 9f956786b91d9786ca11aa5be6104542fa911546
|
||||
- name: golang.org/x/net
|
||||
version: fb93926129b8ec0056f2f458b1f519654814edf0
|
||||
subpackages:
|
||||
|
|
@ -30,7 +34,7 @@ imports:
|
|||
- http2/hpack
|
||||
- internal/timeseries
|
||||
- name: google.golang.org/grpc
|
||||
version: 8eeecf2291de9d171d0b1392a27ff3975679f4f5
|
||||
version: dec33edc378cf4971a2741cfd86ed70a644d6ba3
|
||||
subpackages:
|
||||
- codes
|
||||
- credentials
|
||||
|
|
|
|||
|
|
@ -13,3 +13,4 @@ import:
|
|||
- package: github.com/Masterminds/semver
|
||||
version: 1.1.0
|
||||
- package: github.com/BurntSushi/toml
|
||||
- package: github.com/technosophos/moniker
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import (
|
|||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/deis/tiller/pkg/hapi"
|
||||
chartutil "github.com/deis/tiller/pkg/chart"
|
||||
"github.com/deis/tiller/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
// Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates.
|
||||
|
|
@ -38,13 +39,41 @@ func New() *Engine {
|
|||
//
|
||||
// This will look in the chart's 'templates' data (e.g. the 'templates/' directory)
|
||||
// and attempt to render the templates there using the values passed in.
|
||||
func (e *Engine) Render(chart *hapi.Chart, vals *hapi.Values) (map[string]string, error) {
|
||||
// Uncomment this once the proto files compile.
|
||||
//return render(chart.Chartfile.Name, chart.Templates, vals)
|
||||
return map[string]string{}, nil
|
||||
func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config) (map[string]string, error) {
|
||||
var cvals chartutil.Values
|
||||
if chrt.Values == nil {
|
||||
cvals = map[string]interface{}{}
|
||||
} else {
|
||||
var err error
|
||||
cvals, err = chartutil.ReadValues([]byte(chrt.Values.Raw))
|
||||
if err != nil {
|
||||
return map[string]string{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse values if not nil
|
||||
if vals != nil {
|
||||
evals, err := chartutil.ReadValues([]byte(vals.Raw))
|
||||
if err != nil {
|
||||
return map[string]string{}, err
|
||||
}
|
||||
// Coalesce chart default values and values
|
||||
for k, v := range evals {
|
||||
// FIXME: This needs to merge tables. Ideally, this feature should
|
||||
// be part of the Values type.
|
||||
cvals[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Render the charts
|
||||
tmap := make(map[string]string, len(chrt.Templates))
|
||||
for _, tpl := range chrt.Templates {
|
||||
tmap[tpl.Name] = string(tpl.Data)
|
||||
}
|
||||
return e.render(tmap, cvals)
|
||||
}
|
||||
|
||||
func (e *Engine) render(name string, tpls map[string]string, v interface{}) (map[string]string, error) {
|
||||
func (e *Engine) render(tpls map[string]string, v interface{}) (map[string]string, error) {
|
||||
// Basically, what we do here is start with an empty parent template and then
|
||||
// build up a list of templates -- one for each file. Once all of the templates
|
||||
// have been parsed, we loop through again and execute every template.
|
||||
|
|
@ -52,7 +81,7 @@ func (e *Engine) render(name string, tpls map[string]string, v interface{}) (map
|
|||
// The idea with this process is to make it possible for more complex templates
|
||||
// to share common blocks, but to make the entire thing feel like a file-based
|
||||
// template engine.
|
||||
t := template.New(name)
|
||||
t := template.New("gotpl")
|
||||
files := []string{}
|
||||
for fname, tpl := range tpls {
|
||||
t = t.New(fname).Funcs(e.FuncMap)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func TestRenderInternals(t *testing.T) {
|
|||
}
|
||||
vals := map[string]string{"Name": "one", "Value": "two"}
|
||||
|
||||
out, err := e.render("irrelevant", tpls, vals)
|
||||
out, err := e.render(tpls, vals)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed template rendering: %s", err)
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ func TestParallelRenderInternals(t *testing.T) {
|
|||
tt := fmt.Sprintf("expect-%d", i)
|
||||
tpls := map[string]string{fname: `{{.val}}`}
|
||||
v := map[string]string{"val": tt}
|
||||
out, err := e.render("intentionally_duplicated", tpls, v)
|
||||
out, err := e.render(tpls, v)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to render %s: %s", tt, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ It has these top-level messages:
|
|||
Config
|
||||
Maintainer
|
||||
Metadata
|
||||
Templates
|
||||
Template
|
||||
*/
|
||||
package chart
|
||||
|
|
@ -43,7 +42,7 @@ type Chart struct {
|
|||
// Contents of the Chartfile.
|
||||
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata" json:"metadata,omitempty"`
|
||||
// Templates for this chart.
|
||||
Templates *Templates `protobuf:"bytes,2,opt,name=templates" json:"templates,omitempty"`
|
||||
Templates []*Template `protobuf:"bytes,2,rep,name=templates" json:"templates,omitempty"`
|
||||
// Charts that this chart depends on.
|
||||
Dependencies []*Chart `protobuf:"bytes,3,rep,name=dependencies" json:"dependencies,omitempty"`
|
||||
// Default config for this template.
|
||||
|
|
@ -62,7 +61,7 @@ func (m *Chart) GetMetadata() *Metadata {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Chart) GetTemplates() *Templates {
|
||||
func (m *Chart) GetTemplates() []*Template {
|
||||
if m != nil {
|
||||
return m.Templates
|
||||
}
|
||||
|
|
@ -88,18 +87,18 @@ func init() {
|
|||
}
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 200 bytes of a gzipped FileDescriptorProto
|
||||
// 197 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcb, 0x48, 0x2c, 0xc8,
|
||||
0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0x81, 0x90, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x5c,
|
||||
0x20, 0x71, 0x3d, 0xb0, 0x88, 0x94, 0x38, 0xb2, 0x9a, 0xfc, 0xbc, 0xb4, 0xcc, 0x74, 0x88, 0x22,
|
||||
0x29, 0x49, 0x24, 0x89, 0xdc, 0xd4, 0x92, 0xc4, 0x94, 0xc4, 0x92, 0x44, 0x2c, 0x52, 0x25, 0xa9,
|
||||
0xb9, 0x05, 0x39, 0x89, 0x25, 0xa9, 0x10, 0x29, 0xa5, 0x8b, 0x8c, 0x5c, 0xac, 0xce, 0x20, 0x09,
|
||||
0xb9, 0x05, 0x39, 0x89, 0x25, 0xa9, 0x10, 0x29, 0xa5, 0x0b, 0x8c, 0x5c, 0xac, 0xce, 0x20, 0x09,
|
||||
0x21, 0x03, 0x2e, 0x0e, 0x98, 0x36, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x6e, 0x23, 0x11, 0x3d, 0x84,
|
||||
0xbd, 0x7a, 0xbe, 0x50, 0xb9, 0x20, 0xb8, 0x2a, 0x21, 0x63, 0x2e, 0x4e, 0x98, 0x69, 0xc5, 0x12,
|
||||
0x4c, 0x60, 0x2d, 0xa2, 0xc8, 0x5a, 0x42, 0x60, 0x92, 0x41, 0x08, 0x75, 0x42, 0xa6, 0x5c, 0x3c,
|
||||
0x29, 0xa9, 0x05, 0xa9, 0x79, 0x29, 0xa9, 0x79, 0xc9, 0x99, 0x40, 0x7d, 0xcc, 0x0a, 0xcc, 0x40,
|
||||
0x7d, 0x82, 0xc8, 0xfa, 0xc0, 0xee, 0x09, 0x42, 0x51, 0x26, 0xa4, 0xc5, 0xc5, 0x56, 0x96, 0x98,
|
||||
0x53, 0x0a, 0xd4, 0xc0, 0x02, 0xb6, 0x48, 0x08, 0x45, 0x03, 0x38, 0x1c, 0x82, 0xa0, 0x2a, 0x9c,
|
||||
0xd8, 0xa3, 0x58, 0xc1, 0xe2, 0x49, 0x6c, 0x60, 0x3f, 0x1a, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff,
|
||||
0x12, 0xa6, 0x6a, 0xa8, 0x58, 0x01, 0x00, 0x00,
|
||||
0xbd, 0x7a, 0xbe, 0x50, 0xb9, 0x20, 0xb8, 0x2a, 0x21, 0x23, 0x2e, 0x4e, 0x98, 0x69, 0xc5, 0x12,
|
||||
0x4c, 0x0a, 0xcc, 0xe8, 0x5a, 0x42, 0xa0, 0x92, 0x41, 0x08, 0x65, 0x42, 0xa6, 0x5c, 0x3c, 0x29,
|
||||
0xa9, 0x05, 0xa9, 0x79, 0x29, 0xa9, 0x79, 0xc9, 0x99, 0x40, 0x6d, 0xcc, 0x60, 0x6d, 0x82, 0xc8,
|
||||
0xda, 0xc0, 0xce, 0x09, 0x42, 0x51, 0x26, 0xa4, 0xc5, 0xc5, 0x56, 0x96, 0x98, 0x53, 0x0a, 0xd4,
|
||||
0xc0, 0x02, 0x76, 0x9a, 0x10, 0x8a, 0x06, 0x70, 0x30, 0x04, 0x41, 0x55, 0x38, 0xb1, 0x47, 0xb1,
|
||||
0x82, 0xc5, 0x93, 0xd8, 0xc0, 0x5e, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb5, 0xff, 0x0f,
|
||||
0xec, 0x57, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,55 +13,33 @@ var _ = proto.Marshal
|
|||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// Template represents a template as a name/value pair.
|
||||
//
|
||||
// Template:
|
||||
//
|
||||
// TODO
|
||||
//
|
||||
type Templates struct {
|
||||
// TODO
|
||||
Templates []*Template `protobuf:"bytes,1,rep,name=templates" json:"templates,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Templates) Reset() { *m = Templates{} }
|
||||
func (m *Templates) String() string { return proto.CompactTextString(m) }
|
||||
func (*Templates) ProtoMessage() {}
|
||||
func (*Templates) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} }
|
||||
|
||||
func (m *Templates) GetTemplates() []*Template {
|
||||
if m != nil {
|
||||
return m.Templates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// By convention, name is a relative path within the scope of the chart's
|
||||
// base directory.
|
||||
type Template struct {
|
||||
// TODO
|
||||
TemplateName string `protobuf:"bytes,1,opt,name=template_name,json=templateName" json:"template_name,omitempty"`
|
||||
// TODO
|
||||
TemplateData []byte `protobuf:"bytes,2,opt,name=template_data,json=templateData,proto3" json:"template_data,omitempty"`
|
||||
// Name is the path-like name of the template.
|
||||
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
|
||||
// Data is the template as byte data.
|
||||
Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Template) Reset() { *m = Template{} }
|
||||
func (m *Template) String() string { return proto.CompactTextString(m) }
|
||||
func (*Template) ProtoMessage() {}
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{1} }
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} }
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Templates)(nil), "hapi.chart.Templates")
|
||||
proto.RegisterType((*Template)(nil), "hapi.chart.Template")
|
||||
}
|
||||
|
||||
var fileDescriptor3 = []byte{
|
||||
// 146 bytes of a gzipped FileDescriptorProto
|
||||
// 106 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8,
|
||||
0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x2f, 0x49, 0xcd, 0x2d, 0xc8, 0x49, 0x2c, 0x49, 0xd5,
|
||||
0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe9, 0x81, 0xa5, 0x94, 0xec, 0xb9, 0x38,
|
||||
0x43, 0xa0, 0xb2, 0xc5, 0x42, 0x46, 0x5c, 0x9c, 0x30, 0xa5, 0xc5, 0x12, 0x8c, 0x0a, 0xcc, 0x1a,
|
||||
0xdc, 0x46, 0x22, 0x7a, 0x08, 0xc5, 0x7a, 0x30, 0x95, 0x41, 0x08, 0x65, 0x4a, 0x21, 0x5c, 0x1c,
|
||||
0x30, 0x61, 0x21, 0x65, 0x2e, 0x5e, 0x98, 0x44, 0x7c, 0x5e, 0x62, 0x6e, 0x2a, 0xd0, 0x0c, 0x46,
|
||||
0x0d, 0xce, 0x20, 0x1e, 0x98, 0xa0, 0x1f, 0x50, 0x0c, 0x45, 0x51, 0x4a, 0x62, 0x49, 0xa2, 0x04,
|
||||
0x13, 0x50, 0x11, 0x0f, 0x42, 0x91, 0x0b, 0x50, 0xcc, 0x89, 0x3d, 0x8a, 0x15, 0x6c, 0x65, 0x12,
|
||||
0x1b, 0xd8, 0xc9, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x48, 0xda, 0x77, 0x0e, 0xcf, 0x00,
|
||||
0x00, 0x00,
|
||||
0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe9, 0x81, 0xa5, 0x94, 0x8c, 0xb8, 0x38,
|
||||
0x42, 0xa0, 0xb2, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a,
|
||||
0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x09, 0x28, 0xc6, 0x13, 0x04,
|
||||
0x66, 0x3b, 0xb1, 0x47, 0xb1, 0x82, 0x35, 0x27, 0xb1, 0x81, 0xcd, 0x33, 0x06, 0x04, 0x00, 0x00,
|
||||
0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue