Commit d644c220 authored by Matt Butcher's avatar Matt Butcher

Merge pull request #643 from technosophos/feat/install-k8s

feat(helm, tiller): implement k8s portion of install
parents f7272f43 6db7c39b
...@@ -12,13 +12,16 @@ option go_package = "release"; ...@@ -12,13 +12,16 @@ option go_package = "release";
// //
message Status { message Status {
enum Code { enum Code {
// Status_UNKNOWN indicates that a release is in an uncertain state.
UNKNOWN = 0; UNKNOWN = 0;
// Status_DEPLOYED indicates that the release has been pushed to Kubernetes.
DEPLOYED = 1; DEPLOYED = 1;
// Status_DELETED indicates that a release has been deleted from Kubermetes.
DELETED = 2; DELETED = 2;
// Status_SUPERSEDED indicates that this release object is outdated and a newer one exists.
SUPERSEDED = 3; SUPERSEDED = 3;
// Status_FAILED indicates that the release was not successfully deployed.
FAILED = 4;
} }
Code code = 1; Code code = 1;
......
...@@ -3,11 +3,13 @@ package main ...@@ -3,11 +3,13 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/kubernetes/helm/pkg/helm" "github.com/kubernetes/helm/pkg/helm"
"github.com/kubernetes/helm/pkg/proto/hapi/release" "github.com/kubernetes/helm/pkg/proto/hapi/release"
"github.com/kubernetes/helm/pkg/timeconv"
) )
const installDesc = ` const installDesc = `
...@@ -25,9 +27,14 @@ const ( ...@@ -25,9 +27,14 @@ const (
// install flags & args // install flags & args
var ( var (
installArg string // name or relative path of the chart to install // installArg is the name or relative path of the chart to install
tillerHost string // override TILLER_HOST envVar installArg string
verbose bool // enable verbose install // tillerHost overrides TILLER_HOST envVar
tillerHost string
// verbose enables verbose output
verbose bool
// installDryRun performs a dry-run install
installDryRun bool
) )
var installCmd = &cobra.Command{ var installCmd = &cobra.Command{
...@@ -40,7 +47,7 @@ var installCmd = &cobra.Command{ ...@@ -40,7 +47,7 @@ var installCmd = &cobra.Command{
func runInstall(cmd *cobra.Command, args []string) error { func runInstall(cmd *cobra.Command, args []string) error {
setupInstallEnv(args) setupInstallEnv(args)
res, err := helm.InstallRelease(installArg) res, err := helm.InstallRelease(installArg, installDryRun)
if err != nil { if err != nil {
return err return err
} }
...@@ -59,8 +66,9 @@ func printRelease(rel *release.Release) { ...@@ -59,8 +66,9 @@ func printRelease(rel *release.Release) {
} }
fmt.Printf("release.name: %s\n", rel.Name) fmt.Printf("release.name: %s\n", rel.Name)
if verbose { if verbose {
fmt.Printf("release.info: %s\n", rel.GetInfo()) fmt.Printf("release.info: %s %s\n", timeconv.Format(rel.Info.LastDeployed, time.ANSIC), rel.Info.Status)
fmt.Printf("release.chart: %s\n", rel.GetChart()) fmt.Printf("release.chart: %s %s\n", rel.Chart.Metadata.Name, rel.Chart.Metadata.Version)
fmt.Printf("release.manifest: %s\n", rel.Manifest)
} }
} }
...@@ -92,6 +100,7 @@ func fatalf(format string, args ...interface{}) { ...@@ -92,6 +100,7 @@ func fatalf(format string, args ...interface{}) {
func init() { func init() {
installCmd.Flags().StringVar(&tillerHost, "host", defaultHost, "address of tiller server") installCmd.Flags().StringVar(&tillerHost, "host", defaultHost, "address of tiller server")
installCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose install") installCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose install")
installCmd.Flags().BoolVar(&installDryRun, "dry-run", false, "simulate an install")
RootCommand.AddCommand(installCmd) RootCommand.AddCommand(installCmd)
} }
/*Package environment describes the operating environment for Tiller.
Tiller's environment encapsulates all of the service dependencies Tiller has.
These dependencies are expressed as interfaces so that alternate implementations
(mocks, etc.) can be easily generated.
*/
package environment package environment
import ( import (
"io"
"github.com/kubernetes/helm/pkg/engine" "github.com/kubernetes/helm/pkg/engine"
"github.com/kubernetes/helm/pkg/kube"
"github.com/kubernetes/helm/pkg/proto/hapi/chart" "github.com/kubernetes/helm/pkg/proto/hapi/chart"
"github.com/kubernetes/helm/pkg/proto/hapi/release" "github.com/kubernetes/helm/pkg/proto/hapi/release"
"github.com/kubernetes/helm/pkg/storage" "github.com/kubernetes/helm/pkg/storage"
...@@ -10,6 +19,9 @@ import ( ...@@ -10,6 +19,9 @@ import (
// GoTplEngine is the name of the Go template engine, as registered in the EngineYard. // GoTplEngine is the name of the Go template engine, as registered in the EngineYard.
const GoTplEngine = "gotpl" const GoTplEngine = "gotpl"
// DefaultNamespace is the default namespace for Tiller.
const DefaultNamespace = "helm"
// DefaultEngine points to the engine that the EngineYard should treat as the // DefaultEngine points to the engine that the EngineYard should treat as the
// default. A chart that does not specify an engine may be run through the // default. A chart that does not specify an engine may be run through the
// default engine. // default engine.
...@@ -52,7 +64,11 @@ func (y EngineYard) Default() Engine { ...@@ -52,7 +64,11 @@ func (y EngineYard) Default() Engine {
// An Engine must be capable of executing multiple concurrent requests, but // An Engine must be capable of executing multiple concurrent requests, but
// without tainting one request's environment with data from another request. // without tainting one request's environment with data from another request.
type Engine interface { type Engine interface {
Render(*chart.Chart, *chart.Config) (map[string]string, error) // Render renders a chart.
//
// It receives a chart, a config, and a map of overrides to the config.
// Overrides are assumed to be passed from the system, not the user.
Render(*chart.Chart, *chart.Config, map[string]interface{}) (map[string]string, error)
} }
// ReleaseStorage represents a storage engine for a Release. // ReleaseStorage represents a storage engine for a Release.
...@@ -106,19 +122,37 @@ type ReleaseStorage interface { ...@@ -106,19 +122,37 @@ type ReleaseStorage interface {
// //
// A KubeClient must be concurrency safe. // A KubeClient must be concurrency safe.
type KubeClient interface { type KubeClient interface {
// Install takes a map where the key is a "file name" (read: unique relational // Create creates one or more resources.
// id) and the value is a Kubernetes manifest containing one or more resource //
// definitions. // namespace must contain a valid existing namespace.
// //
// TODO: Can these be in YAML or JSON, or must they be in one particular // reader must contain a YAML stream (one or more YAML documents separated
// format? // by "\n---\n").
Install(manifests map[string]string) error //
// config is optional. If nil, the client will use its existing configuration.
// If set, the client will override its default configuration with the
// passed in one.
Create(namespace string, reader io.Reader) error
}
// PrintingKubeClient implements KubeClient, but simply prints the reader to
// the given output.
type PrintingKubeClient struct {
Out io.Writer
}
// Create prints the values of what would be created with a real KubeClient.
func (p *PrintingKubeClient) Create(ns string, r io.Reader) error {
_, err := io.Copy(p.Out, r)
return err
} }
// Environment provides the context for executing a client request. // Environment provides the context for executing a client request.
// //
// All services in a context are concurrency safe. // All services in a context are concurrency safe.
type Environment struct { type Environment struct {
// The default namespace
Namespace string
// EngineYard provides access to the known template engines. // EngineYard provides access to the known template engines.
EngineYard EngineYard EngineYard EngineYard
// Releases stores records of releases. // Releases stores records of releases.
...@@ -136,7 +170,9 @@ func New() *Environment { ...@@ -136,7 +170,9 @@ func New() *Environment {
GoTplEngine: e, GoTplEngine: e,
} }
return &Environment{ return &Environment{
Namespace: DefaultNamespace,
EngineYard: ey, EngineYard: ey,
Releases: storage.NewMemory(), Releases: storage.NewMemory(),
KubeClient: kube.New(nil), //&PrintingKubeClient{Out: os.Stdout},
} }
} }
package environment package environment
import ( import (
"bytes"
"io"
"testing" "testing"
"github.com/kubernetes/helm/pkg/proto/hapi/chart" "github.com/kubernetes/helm/pkg/proto/hapi/chart"
...@@ -11,7 +13,7 @@ type mockEngine struct { ...@@ -11,7 +13,7 @@ type mockEngine struct {
out map[string]string out map[string]string
} }
func (e *mockEngine) Render(chrt *chart.Chart, v *chart.Config) (map[string]string, error) { func (e *mockEngine) Render(chrt *chart.Chart, v *chart.Config, o map[string]interface{}) (map[string]string, error) {
return e.out, nil return e.out, nil
} }
...@@ -48,13 +50,14 @@ func (r *mockReleaseStorage) Query(labels map[string]string) ([]*release.Release ...@@ -48,13 +50,14 @@ func (r *mockReleaseStorage) Query(labels map[string]string) ([]*release.Release
type mockKubeClient struct { type mockKubeClient struct {
} }
func (k *mockKubeClient) Install(manifests map[string]string) error { func (k *mockKubeClient) Create(ns string, r io.Reader) error {
return nil return nil
} }
var _ Engine = &mockEngine{} var _ Engine = &mockEngine{}
var _ ReleaseStorage = &mockReleaseStorage{} var _ ReleaseStorage = &mockReleaseStorage{}
var _ KubeClient = &mockKubeClient{} var _ KubeClient = &mockKubeClient{}
var _ KubeClient = &PrintingKubeClient{}
func TestEngine(t *testing.T) { func TestEngine(t *testing.T) {
eng := &mockEngine{out: map[string]string{"albatross": "test"}} eng := &mockEngine{out: map[string]string{"albatross": "test"}}
...@@ -64,7 +67,7 @@ func TestEngine(t *testing.T) { ...@@ -64,7 +67,7 @@ func TestEngine(t *testing.T) {
if engine, ok := env.EngineYard.Get("test"); !ok { if engine, ok := env.EngineYard.Get("test"); !ok {
t.Errorf("failed to get engine from EngineYard") t.Errorf("failed to get engine from EngineYard")
} else if out, err := engine.Render(&chart.Chart{}, &chart.Config{}); err != nil { } else if out, err := engine.Render(&chart.Chart{}, &chart.Config{}, map[string]interface{}{}); err != nil {
t.Errorf("unexpected template error: %s", err) t.Errorf("unexpected template error: %s", err)
} else if out["albatross"] != "test" { } else if out["albatross"] != "test" {
t.Errorf("expected 'test', got %q", out["albatross"]) t.Errorf("expected 'test', got %q", out["albatross"])
...@@ -102,9 +105,18 @@ func TestKubeClient(t *testing.T) { ...@@ -102,9 +105,18 @@ func TestKubeClient(t *testing.T) {
env := New() env := New()
env.KubeClient = kc env.KubeClient = kc
manifests := map[string]string{} manifests := map[string]string{
"foo": "name: value\n",
"bar": "name: value\n",
}
b := bytes.NewBuffer(nil)
for _, content := range manifests {
b.WriteString("\n---\n")
b.WriteString(content)
}
if err := env.KubeClient.Install(manifests); err != nil { if err := env.KubeClient.Create("sharry-bobbins", b); err != nil {
t.Errorf("Kubeclient failed: %s", err) t.Errorf("Kubeclient failed: %s", err)
} }
} }
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/kubernetes/helm/cmd/tiller/environment" "github.com/kubernetes/helm/cmd/tiller/environment"
"github.com/kubernetes/helm/pkg/proto/hapi/release" "github.com/kubernetes/helm/pkg/proto/hapi/release"
"github.com/kubernetes/helm/pkg/proto/hapi/services" "github.com/kubernetes/helm/pkg/proto/hapi/services"
"github.com/kubernetes/helm/pkg/storage"
"github.com/kubernetes/helm/pkg/timeconv" "github.com/kubernetes/helm/pkg/timeconv"
"github.com/technosophos/moniker" "github.com/technosophos/moniker"
ctx "golang.org/x/net/context" ctx "golang.org/x/net/context"
...@@ -99,19 +100,44 @@ func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease ...@@ -99,19 +100,44 @@ func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease
return nil, errNotImplemented return nil, errNotImplemented
} }
func (s *releaseServer) uniqName() (string, error) {
maxTries := 5
for i := 0; i < maxTries; i++ {
namer := moniker.New()
name := namer.NameSep("-")
if _, err := s.env.Releases.Read(name); err == storage.ErrNotFound {
return name, nil
}
log.Printf("info: Name %q is taken. Searching again.", name)
}
log.Printf("warning: No available release names found after %d tries", maxTries)
return "ERROR", errors.New("no available release name found")
}
func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) {
if req.Chart == nil { if req.Chart == nil {
return nil, errMissingChart 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.NameSep("-")
ts := timeconv.Now() ts := timeconv.Now()
name, err := s.uniqName()
if err != nil {
return nil, err
}
overrides := map[string]interface{}{
"Release": map[string]interface{}{
"Name": name,
"Time": ts,
"Namespace": s.env.Namespace,
"Service": "Tiller",
},
"Chart": req.Chart.Metadata,
}
// Render the templates // Render the templates
files, err := s.env.EngineYard.Default().Render(req.Chart, req.Values) // TODO: Fix based on whether chart has `engine: SOMETHING` set.
files, err := s.env.EngineYard.Default().Render(req.Chart, req.Values, overrides)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -139,16 +165,30 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea ...@@ -139,16 +165,30 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
Manifest: b.String(), Manifest: b.String(),
} }
res := &services.InstallReleaseResponse{Release: r}
if req.DryRun { if req.DryRun {
log.Printf("Dry run for %s", name) log.Printf("Dry run for %s", name)
return &services.InstallReleaseResponse{Release: r}, nil return res, nil
} }
if err := s.env.Releases.Create(r); err != nil { if err := s.env.KubeClient.Create(s.env.Namespace, b); err != nil {
return nil, err r.Info.Status.Code = release.Status_FAILED
log.Printf("warning: Release %q failed: %s", name, err)
return res, fmt.Errorf("release %s failed: %s", name, err)
} }
return &services.InstallReleaseResponse{Release: r}, nil // This is a tricky case. The release has been created, but the result
// cannot be recorded. The truest thing to tell the user is that the
// release was created. However, the user will not be able to do anything
// further with this release.
//
// One possible strategy would be to do a timed retry to see if we can get
// this stored in the future.
if err := s.env.Releases.Create(r); err != nil {
log.Printf("warning: Failed to record release %q: %s", name, err)
}
return res, nil
} }
func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) {
......
...@@ -2,6 +2,7 @@ package main ...@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"testing" "testing"
...@@ -219,6 +220,7 @@ func TestListReleases(t *testing.T) { ...@@ -219,6 +220,7 @@ func TestListReleases(t *testing.T) {
func mockEnvironment() *environment.Environment { func mockEnvironment() *environment.Environment {
e := environment.New() e := environment.New()
e.Releases = storage.NewMemory() e.Releases = storage.NewMemory()
e.KubeClient = &environment.PrintingKubeClient{Out: os.Stdout}
return e return e
} }
......
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
name: {{default "alpine" .name}} name: {{.Release.Name}}-{{.Chart.Name}}
labels: labels:
heritage: helm heritage: {{.Release.Service}}
chartName: {{.Chart.Name}}
chartVersion: {{.Chart.Version}}
annotations:
"helm.sh/created": {{.Release.Time.Seconds}}
spec: spec:
restartPolicy: {{default "Never" .restart_policy}} restartPolicy: {{default "Never" .restart_policy}}
containers: containers:
......
...@@ -59,7 +59,7 @@ func New() *Engine { ...@@ -59,7 +59,7 @@ func New() *Engine {
// - Scalar values and arrays are replaced, maps are merged // - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of // - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies. // the values destined for its dependencies.
func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config) (map[string]string, error) { func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config, overrides map[string]interface{}) (map[string]string, error) {
var cvals chartutil.Values var cvals chartutil.Values
// Parse values if not nil. We merge these at the top level because // Parse values if not nil. We merge these at the top level because
...@@ -69,6 +69,12 @@ func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config) (map[string]strin ...@@ -69,6 +69,12 @@ func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config) (map[string]strin
if err != nil { if err != nil {
return map[string]string{}, err return map[string]string{}, err
} }
// Override the top-level values. Overrides are NEVER merged deeply.
// The assumption is that an override is intended to set an explicit
// and exact value.
for k, v := range overrides {
evals[k] = v
}
cvals = coalesceValues(chrt, evals) cvals = coalesceValues(chrt, evals)
} }
......
...@@ -22,7 +22,38 @@ func TestEngine(t *testing.T) { ...@@ -22,7 +22,38 @@ func TestEngine(t *testing.T) {
} }
func TestRender(t *testing.T) { func TestRender(t *testing.T) {
t.Skip() c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Templates: []*chart.Template{
{Name: "test1", Data: []byte("{{.outer | title }} {{.inner | title}}")},
},
Values: &chart.Config{
Raw: `outer = "DEFAULT"\ninner= "DEFAULT"\n`,
},
}
vals := &chart.Config{
Raw: `outer = "BAD"
inner= "inn"`,
}
overrides := map[string]interface{}{
"outer": "spouter",
}
e := New()
out, err := e.Render(c, vals, overrides)
if err != nil {
t.Errorf("Failed to render templates: %s", err)
}
expect := "Spouter Inn"
if out["test1"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test1"])
}
} }
func TestRenderInternals(t *testing.T) { func TestRenderInternals(t *testing.T) {
...@@ -129,7 +160,7 @@ func TestRenderDependency(t *testing.T) { ...@@ -129,7 +160,7 @@ func TestRenderDependency(t *testing.T) {
}, },
} }
out, err := e.Render(ch, nil) out, err := e.Render(ch, nil, map[string]interface{}{})
if err != nil { if err != nil {
t.Fatalf("failed to render chart: %s", err) t.Fatalf("failed to render chart: %s", err)
...@@ -190,7 +221,7 @@ func TestRenderNestedValues(t *testing.T) { ...@@ -190,7 +221,7 @@ func TestRenderNestedValues(t *testing.T) {
what = "flower"`, what = "flower"`,
} }
out, err := e.Render(outer, &inject) out, err := e.Render(outer, &inject, map[string]interface{}{})
if err != nil { if err != nil {
t.Fatalf("failed to render templates: %s", err) t.Fatalf("failed to render templates: %s", err)
} }
......
...@@ -71,7 +71,7 @@ func UninstallRelease(name string) (*services.UninstallReleaseResponse, error) { ...@@ -71,7 +71,7 @@ func UninstallRelease(name string) (*services.UninstallReleaseResponse, error) {
} }
// InstallRelease installs a new chart and returns the release response. // InstallRelease installs a new chart and returns the release response.
func InstallRelease(chStr string) (*services.InstallReleaseResponse, error) { func InstallRelease(chStr string, dryRun bool) (*services.InstallReleaseResponse, error) {
chfi, err := chartutil.LoadChart(chStr) chfi, err := chartutil.LoadChart(chStr)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -90,5 +90,6 @@ func InstallRelease(chStr string) (*services.InstallReleaseResponse, error) { ...@@ -90,5 +90,6 @@ func InstallRelease(chStr string) (*services.InstallReleaseResponse, error) {
return Config.client().install(&services.InstallReleaseRequest{ return Config.client().install(&services.InstallReleaseRequest{
Chart: chpb, Chart: chpb,
Values: vals, Values: vals,
DryRun: dryRun,
}) })
} }
...@@ -17,10 +17,16 @@ var _ = math.Inf ...@@ -17,10 +17,16 @@ var _ = math.Inf
type Status_Code int32 type Status_Code int32
const ( const (
// Status_UNKNOWN indicates that a release is in an uncertain state.
Status_UNKNOWN Status_Code = 0 Status_UNKNOWN Status_Code = 0
// Status_DEPLOYED indicates that the release has been pushed to Kubernetes.
Status_DEPLOYED Status_Code = 1 Status_DEPLOYED Status_Code = 1
// Status_DELETED indicates that a release has been deleted from Kubermetes.
Status_DELETED Status_Code = 2 Status_DELETED Status_Code = 2
// Status_SUPERSEDED indicates that this release object is outdated and a newer one exists.
Status_SUPERSEDED Status_Code = 3 Status_SUPERSEDED Status_Code = 3
// Status_FAILED indicates that the release was not successfully deployed.
Status_FAILED Status_Code = 4
) )
var Status_Code_name = map[int32]string{ var Status_Code_name = map[int32]string{
...@@ -28,12 +34,14 @@ var Status_Code_name = map[int32]string{ ...@@ -28,12 +34,14 @@ var Status_Code_name = map[int32]string{
1: "DEPLOYED", 1: "DEPLOYED",
2: "DELETED", 2: "DELETED",
3: "SUPERSEDED", 3: "SUPERSEDED",
4: "FAILED",
} }
var Status_Code_value = map[string]int32{ var Status_Code_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
"DEPLOYED": 1, "DEPLOYED": 1,
"DELETED": 2, "DELETED": 2,
"SUPERSEDED": 3, "SUPERSEDED": 3,
"FAILED": 4,
} }
func (x Status_Code) String() string { func (x Status_Code) String() string {
...@@ -68,19 +76,20 @@ func init() { ...@@ -68,19 +76,20 @@ func init() {
} }
var fileDescriptor2 = []byte{ var fileDescriptor2 = []byte{
// 215 bytes of a gzipped FileDescriptorProto // 226 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8,
0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6,
0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe9, 0x41, 0xa5, 0xa4, 0x24, 0xd3, 0xf3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe9, 0x41, 0xa5, 0xa4, 0x24, 0xd3, 0xf3,
0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x89, 0x79, 0x95, 0x10, 0x85, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x89, 0x79, 0x95, 0x10, 0x85,
0x4a, 0xcb, 0x19, 0xb9, 0xd8, 0x82, 0xc1, 0x3a, 0x85, 0x74, 0xb9, 0x58, 0x92, 0xf3, 0x53, 0x52, 0x4a, 0x9b, 0x19, 0xb9, 0xd8, 0x82, 0xc1, 0x3a, 0x85, 0x74, 0xb9, 0x58, 0x92, 0xf3, 0x53, 0x52,
0x25, 0x18, 0x15, 0x18, 0x35, 0xf8, 0x8c, 0x24, 0xf5, 0x90, 0x8d, 0xd0, 0x83, 0xa8, 0xd1, 0x73, 0x25, 0x18, 0x15, 0x18, 0x35, 0xf8, 0x8c, 0x24, 0xf5, 0x90, 0x8d, 0xd0, 0x83, 0xa8, 0xd1, 0x73,
0x06, 0x2a, 0x08, 0x02, 0x2b, 0x13, 0xd2, 0xe3, 0x62, 0x4f, 0x49, 0x2d, 0x49, 0xcc, 0xcc, 0x29, 0x06, 0x2a, 0x08, 0x02, 0x2b, 0x13, 0xd2, 0xe3, 0x62, 0x4f, 0x49, 0x2d, 0x49, 0xcc, 0xcc, 0x29,
0x96, 0x60, 0x02, 0xea, 0xe0, 0x36, 0x12, 0xd1, 0x83, 0x58, 0xa3, 0x07, 0xb3, 0x46, 0xcf, 0x31, 0x96, 0x60, 0x02, 0xea, 0xe0, 0x36, 0x12, 0xd1, 0x83, 0x58, 0xa3, 0x07, 0xb3, 0x46, 0xcf, 0x31,
0xaf, 0x32, 0x08, 0xa6, 0x48, 0xc9, 0x8e, 0x8b, 0x05, 0xa4, 0x5b, 0x88, 0x9b, 0x8b, 0x3d, 0xd4, 0xaf, 0x32, 0x08, 0xa6, 0x48, 0xc9, 0x8b, 0x8b, 0x05, 0xa4, 0x5b, 0x88, 0x9b, 0x8b, 0x3d, 0xd4,
0xcf, 0xdb, 0xcf, 0x3f, 0xdc, 0x4f, 0x80, 0x41, 0x88, 0x87, 0x8b, 0xc3, 0xc5, 0x35, 0xc0, 0xc7, 0xcf, 0xdb, 0xcf, 0x3f, 0xdc, 0x4f, 0x80, 0x41, 0x88, 0x87, 0x8b, 0xc3, 0xc5, 0x35, 0xc0, 0xc7,
0x3f, 0xd2, 0xd5, 0x45, 0x80, 0x11, 0x24, 0xe5, 0xe2, 0xea, 0xe3, 0x1a, 0x02, 0xe4, 0x30, 0x09, 0x3f, 0xd2, 0xd5, 0x45, 0x80, 0x11, 0x24, 0xe5, 0xe2, 0xea, 0xe3, 0x1a, 0x02, 0xe4, 0x30, 0x09,
0xf1, 0x71, 0x71, 0x05, 0x87, 0x06, 0xb8, 0x06, 0x05, 0xbb, 0xba, 0x00, 0xf9, 0xcc, 0x4e, 0x9c, 0xf1, 0x71, 0x71, 0x05, 0x87, 0x06, 0xb8, 0x06, 0x05, 0xbb, 0xba, 0x00, 0xf9, 0xcc, 0x42, 0x5c,
0x51, 0xec, 0x50, 0xc7, 0x24, 0xb1, 0x81, 0x6d, 0x30, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x0d, 0x5c, 0x6c, 0x6e, 0x8e, 0x9e, 0x3e, 0x40, 0x36, 0x8b, 0x13, 0x67, 0x14, 0x3b, 0xd4, 0x61, 0x49,
0xcd, 0xe7, 0x6f, 0x01, 0x01, 0x00, 0x00, 0x6c, 0x60, 0xdb, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x8c, 0x99, 0x9a, 0x3b, 0x0d, 0x01,
0x00, 0x00,
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment