Commit c2459c06 authored by Miguel Martinez's avatar Miguel Martinez

New Chart linter structure

Fixed tests

Adding chart name linter

Add lint error

Moving to blocks

Moving to method

Moved lint rules to functions

Semantic version validation

Linting engine

Adding sources and home validations

Sharing file loading

Sharing file loading

Rolling back readme

Rewriting other linters

Fixing tests

Typo

Using chart.Metadata

Fixing format

Adding UNKNOWN in Engine

Adding tabs

Fixing tabs
parent 7a227440
...@@ -17,6 +17,10 @@ message Maintainer { ...@@ -17,6 +17,10 @@ message Maintainer {
// //
// Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file // Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file
message Metadata { message Metadata {
enum Engine {
UNKNOWN = 0;
GOTPL = 1;
}
// The name of the chart // The name of the chart
string name = 1; string name = 1;
......
...@@ -45,6 +45,11 @@ func lintCmd(cmd *cobra.Command, args []string) error { ...@@ -45,6 +45,11 @@ func lintCmd(cmd *cobra.Command, args []string) error {
} }
issues := lint.All(path) issues := lint.All(path)
if len(issues) == 0 {
fmt.Println("Lint OK")
}
for _, i := range issues { for _, i := range issues {
fmt.Printf("%s\n", i) fmt.Printf("%s\n", i)
} }
......
...@@ -4,5 +4,3 @@ version: 0.1.0 ...@@ -4,5 +4,3 @@ version: 0.1.0
home: https://k8s.io/helm home: https://k8s.io/helm
sources: sources:
- https://github.com/kubernetes/helm - https://github.com/kubernetes/helm
...@@ -5,6 +5,8 @@ imports: ...@@ -5,6 +5,8 @@ imports:
version: 75cd24fc2f2c version: 75cd24fc2f2c
- name: github.com/aokoli/goutils - name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64 version: 9c37978a95bd5c709a15883b6242714ea6709e64
- name: github.com/asaskevich/govalidator
version: df81827fdd59d8b4fb93d8910b286ab7a3919520
- name: github.com/beorn7/perks - name: github.com/beorn7/perks
version: b965b613227fddccbfffe13eae360ed3fa822f8d version: b965b613227fddccbfffe13eae360ed3fa822f8d
subpackages: subpackages:
......
...@@ -38,3 +38,4 @@ import: ...@@ -38,3 +38,4 @@ import:
- package: speter.net/go/exp/math/dec/inf - package: speter.net/go/exp/math/dec/inf
repo: https://github.com/go-inf/inf.git repo: https://github.com/go-inf/inf.git
vcs: git vcs: git
- package: github.com/asaskevich/govalidator
package lint
import (
"os"
"path/filepath"
"k8s.io/helm/pkg/chartutil"
)
// Chartfile checks the Chart.yaml file for errors and warnings.
func Chartfile(basepath string) (m []Message) {
m = []Message{}
path := filepath.Join(basepath, "Chart.yaml")
if fi, err := os.Stat(path); err != nil {
m = append(m, Message{Severity: ErrorSev, Text: "Chart.yaml file: " + path + " does not exist"})
return
} else if fi.IsDir() {
m = append(m, Message{Severity: ErrorSev, Text: "Chart.yaml is a directory."})
return
}
cf, err := chartutil.LoadChartfile(path)
if err != nil {
m = append(m, Message{
Severity: ErrorSev,
Text: err.Error(),
})
return
}
if cf.Name == "" {
m = append(m, Message{
Severity: ErrorSev,
Text: "Chart.yaml: 'name' is required",
})
}
if cf.Version == "" || cf.Version == "0.0.0" {
m = append(m, Message{
Severity: ErrorSev,
Text: "Chart.yaml: 'version' is required, and must be greater than 0.0.0",
})
}
return
}
package lint package lint
import (
"k8s.io/helm/pkg/lint/rules"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
)
// All runs all of the available linters on the given base directory. // All runs all of the available linters on the given base directory.
func All(basedir string) []Message { func All(basedir string) []support.Message {
out := Chartfile(basedir) // Using abs path to get directory context
out = append(out, Templates(basedir)...) current, _ := os.Getwd()
out = append(out, Values(basedir)...) chartDir := filepath.Join(current, basedir)
return out
linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.Values(&linter)
rules.Templates(&linter)
return linter.Messages
} }
package lint package lint
import ( import (
"k8s.io/helm/pkg/lint/support"
"strings" "strings"
"testing" "testing"
) )
const badChartDir = "testdata/badchartfile" const badChartDir = "rules/testdata/badchartfile"
const badValuesFileDir = "testdata/badvaluesfile" const badValuesFileDir = "rules/testdata/badvaluesfile"
const badYamlFileDir = "testdata/albatross" const badYamlFileDir = "rules/testdata/albatross"
const goodChartDir = "testdata/goodone" const goodChartDir = "rules/testdata/goodone"
func TestBadChart(t *testing.T) { func TestBadChart(t *testing.T) {
m := All(badChartDir) m := All(badChartDir)
if len(m) != 3 { if len(m) != 4 {
t.Errorf("Number of errors %v", len(m))
t.Errorf("All didn't fail with expected errors, got %#v", m) t.Errorf("All didn't fail with expected errors, got %#v", m)
} }
// There should be 2 WARNINGs and one ERROR messages, check for them // There should be 2 WARNINGs and one ERROR messages, check for them
var w, e, e2 = false, false, false var w, e, e2, e3 bool
for _, msg := range m { for _, msg := range m {
if msg.Severity == WarningSev { if msg.Severity == support.WarningSev {
if strings.Contains(msg.Text, "No templates") { if strings.Contains(msg.Text, "Templates directory not found") {
w = true w = true
} }
} }
if msg.Severity == ErrorSev { if msg.Severity == support.ErrorSev {
if strings.Contains(msg.Text, "must be greater than 0.0.0") { if strings.Contains(msg.Text, "'version' 0.0.0 is less than or equal to 0") {
e = true e = true
} }
if strings.Contains(msg.Text, "'name' is required") { if strings.Contains(msg.Text, "'name' is required") {
e2 = true e2 = true
} }
if strings.Contains(msg.Text, "'name' and directory do not match") {
e3 = true
}
} }
} }
if !e || !e2 || !w { if !e || !e2 || !e3 || !w {
t.Errorf("Didn't find all the expected errors, got %#v", m) t.Errorf("Didn't find all the expected errors, got %#v", m)
} }
} }
......
package rules
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver"
"github.com/asaskevich/govalidator"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/proto/hapi/chart"
)
// Chartfile runs a set of linter rules related to Chart.yaml file
func Chartfile(linter *support.Linter) {
chartPath := filepath.Join(linter.ChartDir, "Chart.yaml")
linter.RunLinterRule(support.ErrorSev, validateChartYamlFileExistence(linter, chartPath))
linter.RunLinterRule(support.ErrorSev, validateChartYamlNotDirectory(linter, chartPath))
chartFile, err := chartutil.LoadChartfile(chartPath)
validChartFile := linter.RunLinterRule(support.ErrorSev, validateChartYamlFormat(linter, err))
// Guard clause. Following linter rules require a parseable ChartFile
if !validChartFile {
return
}
linter.RunLinterRule(support.ErrorSev, validateChartName(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartNameDirMatch(linter, chartFile))
// Chart metadata
linter.RunLinterRule(support.ErrorSev, validateChartVersion(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartEngine(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartMaintainer(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartSources(linter, chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartHome(linter, chartFile))
}
// Auxiliar validation methods
func validateChartYamlFileExistence(linter *support.Linter, chartPath string) (lintError support.LintError) {
_, err := os.Stat(chartPath)
if err != nil {
lintError = fmt.Errorf("Chart.yaml file does not exists")
}
return
}
func validateChartYamlNotDirectory(linter *support.Linter, chartPath string) (lintError support.LintError) {
fi, err := os.Stat(chartPath)
if err == nil && fi.IsDir() {
lintError = fmt.Errorf("Chart.yaml is a directory")
}
return
}
func validateChartYamlFormat(linter *support.Linter, chartFileError error) (lintError support.LintError) {
if chartFileError != nil {
lintError = fmt.Errorf("Chart.yaml is malformed: %s", chartFileError.Error())
}
return
}
func validateChartName(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Name == "" {
lintError = fmt.Errorf("Chart.yaml: 'name' is required")
}
return
}
func validateChartNameDirMatch(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Name != filepath.Base(linter.ChartDir) {
lintError = fmt.Errorf("Chart.yaml: 'name' and directory do not match")
}
return
}
func validateChartVersion(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Version == "" {
lintError = fmt.Errorf("Chart.yaml: 'version' value is required")
return
}
version, err := semver.NewVersion(cf.Version)
if err != nil {
lintError = fmt.Errorf("Chart.yaml: version '%s' is not a valid SemVer", cf.Version)
return
}
c, err := semver.NewConstraint("> 0")
valid, msg := c.Validate(version)
if !valid && len(msg) > 0 {
lintError = fmt.Errorf("Chart.yaml: 'version' %v", msg[0])
}
return
}
func validateChartEngine(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Engine == "" {
return
}
keys := make([]string, 0, len(chart.Metadata_Engine_value))
for engine := range chart.Metadata_Engine_value {
str := strings.ToLower(engine)
if str == "unknown" {
continue
}
if str == cf.Engine {
return
}
keys = append(keys, str)
}
lintError = fmt.Errorf("Chart.yaml: 'engine %v not valid. Valid options are %v", cf.Engine, keys)
return
}
func validateChartMaintainer(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
for _, maintainer := range cf.Maintainers {
if maintainer.Name == "" {
lintError = fmt.Errorf("Chart.yaml: maintainer requires a name")
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
lintError = fmt.Errorf("Chart.yaml: maintainer invalid email")
}
}
return
}
func validateChartSources(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
for _, source := range cf.Sources {
if source == "" || !govalidator.IsRequestURL(source) {
lintError = fmt.Errorf("Chart.yaml: 'source' invalid URL %s", source)
}
}
return
}
func validateChartHome(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) {
if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) {
lintError = fmt.Errorf("Chart.yaml: 'home' invalid URL %s", cf.Home)
}
return
}
package lint package rules
import ( import (
"k8s.io/helm/pkg/lint/support"
"strings"
"testing" "testing"
) )
const badchartfile = "testdata/badchartfile" const badchartfile = "testdata/badchartfile"
func TestChartfile(t *testing.T) { func TestChartfile(t *testing.T) {
msgs := Chartfile(badchartfile) linter := support.Linter{ChartDir: badchartfile}
if len(msgs) != 2 { Chartfile(&linter)
t.Errorf("Expected 2 errors, got %d", len(msgs)) msgs := linter.Messages
if len(msgs) != 3 {
t.Errorf("Expected 3 errors, got %d", len(msgs))
} }
if msgs[0].Text != "Chart.yaml: 'name' is required" { if !strings.Contains(msgs[0].Text, "'name' is required") {
t.Errorf("Unexpected message 0: %s", msgs[0].Text) t.Errorf("Unexpected message 0: %s", msgs[0].Text)
} }
if msgs[1].Text != "Chart.yaml: 'version' is required, and must be greater than 0.0.0" { if !strings.Contains(msgs[1].Text, "'name' and directory do not match") {
t.Errorf("Unexpected message 1: %s", msgs[1].Text) t.Errorf("Unexpected message 1: %s", msgs[1].Text)
} }
if !strings.Contains(msgs[2].Text, "'version' 0.0.0 is less than or equal to 0") {
t.Errorf("Unexpected message 2: %s", msgs[2].Text)
}
} }
package rules
import (
"fmt"
"github.com/Masterminds/sprig"
"io/ioutil"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
"text/template"
)
// Templates lints a chart's templates.
func Templates(linter *support.Linter) {
templatespath := filepath.Join(linter.ChartDir, "templates")
templatesExist := linter.RunLinterRule(support.WarningSev, validateTemplatesExistence(linter, templatespath))
// Templates directory is optional for now
if !templatesExist {
return
}
linter.RunLinterRule(support.ErrorSev, validateTemplatesDir(linter, templatespath))
linter.RunLinterRule(support.ErrorSev, validateTemplatesParseable(linter, templatespath))
}
func validateTemplatesExistence(linter *support.Linter, templatesPath string) (lintError support.LintError) {
if _, err := os.Stat(templatesPath); err != nil {
lintError = fmt.Errorf("Templates directory not found")
}
return
}
func validateTemplatesDir(linter *support.Linter, templatesPath string) (lintError support.LintError) {
fi, err := os.Stat(templatesPath)
if err == nil && !fi.IsDir() {
lintError = fmt.Errorf("'templates' is not a directory")
}
return
}
func validateTemplatesParseable(linter *support.Linter, templatesPath string) (lintError support.LintError) {
tpl := template.New("tpl").Funcs(sprig.TxtFuncMap())
lintError = filepath.Walk(templatesPath, func(name string, fi os.FileInfo, e error) error {
if e != nil {
return e
}
if fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(name)
if err != nil {
lintError = fmt.Errorf("cannot read %s: %s", name, err)
return lintError
}
newtpl, err := tpl.Parse(string(data))
if err != nil {
lintError = fmt.Errorf("error processing %s: %s", name, err)
return lintError
}
tpl = newtpl
return nil
})
return
}
package lint package rules
import ( import (
"k8s.io/helm/pkg/lint/support"
"strings" "strings"
"testing" "testing"
) )
...@@ -8,10 +9,12 @@ import ( ...@@ -8,10 +9,12 @@ import (
const templateTestBasedir = "./testdata/albatross" const templateTestBasedir = "./testdata/albatross"
func TestTemplate(t *testing.T) { func TestTemplate(t *testing.T) {
res := Templates(templateTestBasedir) linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter)
res := linter.Messages
if len(res) != 1 { if len(res) != 1 {
t.Fatalf("Expected one error, got %d", len(res)) t.Fatalf("Expected one error, got %d, %v", len(res), res)
} }
if !strings.Contains(res[0].Text, "deliberateSyntaxError") { if !strings.Contains(res[0].Text, "deliberateSyntaxError") {
......
package rules
import (
"fmt"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
"os"
"path/filepath"
)
// Values lints a chart's values.yaml file.
func Values(linter *support.Linter) {
vf := filepath.Join(linter.ChartDir, "values.yaml")
fileExists := linter.RunLinterRule(support.InfoSev, validateValuesFileExistence(linter, vf))
if !fileExists {
return
}
linter.RunLinterRule(support.ErrorSev, validateValuesFile(linter, vf))
}
func validateValuesFileExistence(linter *support.Linter, valuesPath string) (lintError support.LintError) {
_, err := os.Stat(valuesPath)
if err != nil {
lintError = fmt.Errorf("values.yaml file does not exists")
}
return
}
func validateValuesFile(linter *support.Linter, valuesPath string) (lintError support.LintError) {
_, err := chartutil.ReadValuesFile(valuesPath)
if err != nil {
lintError = fmt.Errorf("values.yaml is malformed: %s", err.Error())
}
return
}
...@@ -3,4 +3,4 @@ ...@@ -3,4 +3,4 @@
Linting is the process of testing charts for errors or warnings regarding Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance. formatting, compilation, or standards compliance.
*/ */
package lint package support
package lint package support
import "fmt" import "fmt"
...@@ -22,14 +22,33 @@ var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} ...@@ -22,14 +22,33 @@ var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"}
// Message is a linting output message // Message is a linting output message
type Message struct { type Message struct {
// Severity is one of the *Sev constants // Severity is one of the *Sev constants
Severity int Severity Severity
// Text contains the message text // Text contains the message text
Text string Text string
} }
type Linter struct {
Messages []Message
ChartDir string
}
type LintError interface {
error
}
type ValidationFunc func(*Linter) LintError
// String prints a string representation of this Message. // String prints a string representation of this Message.
// //
// Implements fmt.Stringer. // Implements fmt.Stringer.
func (m Message) String() string { func (m Message) String() string {
return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text) return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text)
} }
// Returns true if the validation passed
func (l *Linter) RunLinterRule(severity Severity, lintError LintError) bool {
if lintError != nil {
l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity})
}
return lintError == nil
}
package lint package support
import ( import (
"fmt" "fmt"
......
package lint
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"text/template"
"github.com/Masterminds/sprig"
)
// Templates lints a chart's templates.
func Templates(basepath string) (messages []Message) {
messages = []Message{}
path := filepath.Join(basepath, "templates")
if fi, err := os.Stat(path); err != nil {
messages = append(messages, Message{Severity: WarningSev, Text: "No templates"})
return
} else if !fi.IsDir() {
messages = append(messages, Message{Severity: ErrorSev, Text: "'templates' is not a directory"})
return
}
tpl := template.New("tpl").Funcs(sprig.TxtFuncMap())
err := filepath.Walk(basepath, func(name string, fi os.FileInfo, e error) error {
// If an error is returned, we fail. Non-fatal errors should just be
// added directly to messages.
if e != nil {
return e
}
if fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(name)
if err != nil {
messages = append(messages, Message{
Severity: ErrorSev,
Text: fmt.Sprintf("cannot read %s: %s", name, err),
})
return nil
}
// An error rendering a file should emit a warning.
newtpl, err := tpl.Parse(string(data))
if err != nil {
messages = append(messages, Message{
Severity: ErrorSev,
Text: fmt.Sprintf("error processing %s: %s", name, err),
})
return nil
}
tpl = newtpl
return nil
})
if err != nil {
messages = append(messages, Message{Severity: ErrorSev, Text: err.Error()})
}
return
}
package lint
import (
"os"
"path/filepath"
"k8s.io/helm/pkg/chartutil"
)
// Values lints a chart's values.yaml file.
func Values(basepath string) (messages []Message) {
vf := filepath.Join(basepath, "values.yaml")
messages = []Message{}
if _, err := os.Stat(vf); err != nil {
messages = append(messages, Message{Severity: InfoSev, Text: "No values.yaml file"})
return
}
_, err := chartutil.ReadValuesFile(vf)
if err != nil {
messages = append(messages, Message{Severity: ErrorSev, Text: err.Error()})
}
return messages
}
...@@ -13,6 +13,27 @@ var _ = proto.Marshal ...@@ -13,6 +13,27 @@ var _ = proto.Marshal
var _ = fmt.Errorf var _ = fmt.Errorf
var _ = math.Inf var _ = math.Inf
type Metadata_Engine int32
const (
Metadata_UNKNOWN Metadata_Engine = 0
Metadata_GOTPL Metadata_Engine = 1
)
var Metadata_Engine_name = map[int32]string{
0: "UNKNOWN",
1: "GOTPL",
}
var Metadata_Engine_value = map[string]int32{
"UNKNOWN": 0,
"GOTPL": 1,
}
func (x Metadata_Engine) String() string {
return proto.EnumName(Metadata_Engine_name, int32(x))
}
func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{1, 0} }
// Maintainer describes a Chart maintainer. // Maintainer describes a Chart maintainer.
type Maintainer struct { type Maintainer struct {
// Name is a user name or organization name // Name is a user name or organization name
...@@ -63,23 +84,26 @@ func (m *Metadata) GetMaintainers() []*Maintainer { ...@@ -63,23 +84,26 @@ func (m *Metadata) GetMaintainers() []*Maintainer {
func init() { func init() {
proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer") proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer")
proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata") proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata")
proto.RegisterEnum("hapi.chart.Metadata_Engine", Metadata_Engine_name, Metadata_Engine_value)
} }
var fileDescriptor2 = []byte{ var fileDescriptor2 = []byte{
// 234 bytes of a gzipped FileDescriptorProto // 266 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xbd, 0x4f, 0xc3, 0x40, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0x4b, 0x4b, 0xc4, 0x40,
0x0c, 0xc5, 0x15, 0xda, 0x7c, 0xe0, 0x6c, 0x16, 0xaa, 0x0c, 0x53, 0xd4, 0x89, 0x29, 0x95, 0x40, 0x10, 0x84, 0xdd, 0x47, 0x1e, 0xdb, 0xb9, 0x2c, 0x8d, 0x2c, 0xa3, 0xa7, 0x90, 0x93, 0xa7, 0x2c,
0x42, 0xcc, 0xec, 0x5d, 0x3a, 0xb2, 0x99, 0xc4, 0x22, 0x27, 0x48, 0x2e, 0xba, 0x3b, 0x40, 0xfc, 0x28, 0x88, 0x67, 0x41, 0x3c, 0xe8, 0x66, 0x65, 0x51, 0x04, 0x6f, 0x63, 0xd2, 0x98, 0x41, 0x93,
0xe3, 0xcc, 0x5c, 0xdc, 0xaf, 0x0c, 0x1d, 0x22, 0xbd, 0xf7, 0x7e, 0x79, 0x3e, 0xd9, 0x70, 0xdb, 0x09, 0x33, 0xa3, 0xe2, 0x3f, 0xf1, 0xe7, 0x3a, 0xe9, 0x7d, 0x1e, 0x3c, 0x04, 0xaa, 0xea, 0x4b,
0xf1, 0x68, 0x36, 0x4d, 0xc7, 0x2e, 0x6c, 0x7a, 0x09, 0xdc, 0x72, 0xe0, 0x7a, 0x74, 0x36, 0x58, 0xd7, 0xd0, 0x0d, 0x27, 0xb5, 0xec, 0xd4, 0xbc, 0xac, 0xa5, 0x71, 0xf3, 0x86, 0x9c, 0xac, 0xa4,
0x84, 0x09, 0xd5, 0x8a, 0xd6, 0x4f, 0x00, 0x5b, 0x36, 0x43, 0x88, 0x9f, 0x38, 0x44, 0x58, 0x0e, 0x93, 0x79, 0x67, 0xb4, 0xd3, 0x08, 0x3d, 0xca, 0x19, 0x65, 0x97, 0x00, 0x0b, 0xa9, 0x5a, 0xe7,
0xdc, 0x0b, 0x25, 0x55, 0x72, 0x7f, 0xbd, 0x53, 0x8d, 0x37, 0x90, 0x4a, 0xcf, 0xe6, 0x93, 0xae, 0x3f, 0x32, 0x88, 0x30, 0x6e, 0x65, 0x43, 0x62, 0x90, 0x0e, 0xce, 0x26, 0x2b, 0xd6, 0x78, 0x0c,
0x34, 0xdc, 0x9b, 0xf5, 0x5f, 0x02, 0xc5, 0xf6, 0x30, 0xf6, 0x62, 0x2d, 0x66, 0x9d, 0x8d, 0xd9, 0x01, 0x35, 0x52, 0x7d, 0x88, 0x21, 0x87, 0x6b, 0x93, 0xfd, 0x0e, 0x21, 0x5e, 0x6c, 0x6a, 0xff,
0xbe, 0xa5, 0x1a, 0x09, 0x72, 0x6f, 0xbf, 0x5c, 0x23, 0x9e, 0x16, 0xd5, 0x22, 0xc6, 0x47, 0x3b, 0x1d, 0xf3, 0x59, 0xad, 0x7d, 0xb6, 0x9e, 0x62, 0x8d, 0x02, 0x22, 0xab, 0x3f, 0x4d, 0x49, 0x56,
0x91, 0x6f, 0x71, 0xde, 0xd8, 0x81, 0x96, 0x5a, 0x38, 0x5a, 0xac, 0xa0, 0x6c, 0xc5, 0x37, 0xce, 0x8c, 0xd2, 0x91, 0x8f, 0xb7, 0xb6, 0x27, 0x5f, 0x64, 0xac, 0xd2, 0xad, 0x18, 0xf3, 0xc0, 0xd6,
0x8c, 0x61, 0xa2, 0xa9, 0xd2, 0x79, 0x84, 0x77, 0x50, 0x7c, 0xc8, 0xef, 0x8f, 0x75, 0xad, 0xa7, 0x62, 0x0a, 0x49, 0x45, 0xb6, 0x34, 0xaa, 0x73, 0x3d, 0x0d, 0x98, 0x1e, 0x46, 0x78, 0x0a, 0xf1,
0x4c, 0xc7, 0x9e, 0x3c, 0x3e, 0x43, 0xd9, 0x9f, 0xd6, 0xf3, 0x94, 0x47, 0x5c, 0x3e, 0xac, 0xea, 0x3b, 0xfd, 0x7c, 0x6b, 0x53, 0x59, 0x11, 0x72, 0xed, 0xce, 0xe3, 0x15, 0x24, 0xcd, 0x6e, 0x3d,
0xf3, 0x01, 0xea, 0xf3, 0xf6, 0xbb, 0xf9, 0xaf, 0xb8, 0x82, 0x4c, 0x86, 0xf7, 0xa8, 0xa9, 0xd0, 0x2b, 0x22, 0x8f, 0x93, 0xf3, 0x59, 0xbe, 0x3f, 0x40, 0xbe, 0xdf, 0x7e, 0x75, 0xf8, 0x2b, 0xce,
0x27, 0x0f, 0xee, 0x25, 0x7f, 0x4d, 0xb5, 0xf8, 0x96, 0xe9, 0x31, 0x1f, 0xff, 0x03, 0x00, 0x00, 0x20, 0xa4, 0xf6, 0xcd, 0x6b, 0x11, 0xf3, 0x93, 0x1b, 0x97, 0xa5, 0x10, 0xde, 0xb0, 0xc2, 0x04,
0xff, 0xff, 0x6f, 0x4a, 0x7b, 0xd0, 0x69, 0x01, 0x00, 0x00, 0xa2, 0xa7, 0xe2, 0xae, 0x58, 0x3e, 0x17, 0xd3, 0x23, 0x9c, 0x40, 0x70, 0xbb, 0x7c, 0x7c, 0xb8,
0x9f, 0x0e, 0xae, 0xa3, 0x97, 0x80, 0xab, 0x5f, 0x43, 0x3e, 0xf7, 0xc5, 0x5f, 0x00, 0x00, 0x00,
0xff, 0xff, 0xe5, 0xf8, 0x57, 0xee, 0x8b, 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