terraform: diff is split down into modules
This commit is contained in:
parent
29603f36d2
commit
bc67e7c443
|
@ -831,7 +831,7 @@ func (c *Context) planWalkFn(result *Plan) depgraph.WalkFunc {
|
||||||
|
|
||||||
l.Lock()
|
l.Lock()
|
||||||
if !diff.Empty() {
|
if !diff.Empty() {
|
||||||
result.Diff.Resources[r.Id] = diff
|
result.Diff.RootModule().Resources[r.Id] = diff
|
||||||
}
|
}
|
||||||
l.Unlock()
|
l.Unlock()
|
||||||
|
|
||||||
|
@ -872,7 +872,7 @@ func (c *Context) planDestroyWalkFn(result *Plan) depgraph.WalkFunc {
|
||||||
|
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
result.Diff.Resources[r.Id] = &InstanceDiff{Destroy: true}
|
result.Diff.RootModule().Resources[r.Id] = &InstanceDiff{Destroy: true}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[DEBUG] %s: Not marking for destroy, no ID", r.Id)
|
log.Printf("[DEBUG] %s: Not marking for destroy, no ID", r.Id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1394,8 +1394,8 @@ func TestContextPlan(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Diff.Resources) < 2 {
|
if len(plan.Diff.RootModule().Resources) < 2 {
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
actual := strings.TrimSpace(plan.String())
|
||||||
|
@ -1458,8 +1458,8 @@ func TestContextPlan_nil(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if len(plan.Diff.Resources) != 0 {
|
if len(plan.Diff.RootModule().Resources) != 0 {
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1525,8 +1525,8 @@ func TestContextPlan_count(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Diff.Resources) < 6 {
|
if len(plan.Diff.RootModule().Resources) < 6 {
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
actual := strings.TrimSpace(plan.String())
|
||||||
|
@ -1715,8 +1715,8 @@ func TestContextPlan_destroy(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Diff.Resources) != 2 {
|
if len(plan.Diff.RootModule().Resources) != 2 {
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
actual := strings.TrimSpace(plan.String())
|
||||||
|
@ -1880,8 +1880,8 @@ func TestContextPlan_state(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Diff.Resources) < 2 {
|
if len(plan.Diff.RootModule().Resources) < 2 {
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
actual := strings.TrimSpace(plan.String())
|
||||||
|
|
|
@ -1,29 +1,98 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Diff tracks the differences between resources to apply.
|
// Diff trackes the changes that are necessary to apply a configuration
|
||||||
|
// to an existing infrastructure.
|
||||||
type Diff struct {
|
type Diff struct {
|
||||||
Resources map[string]*InstanceDiff
|
// Modules contains all the modules that have a diff
|
||||||
once sync.Once
|
Modules []*ModuleDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleByPath is used to lookup the module diff for the given path.
|
||||||
|
// This should be the prefered lookup mechanism as it allows for future
|
||||||
|
// lookup optimizations.
|
||||||
|
func (d *Diff) ModuleByPath(path []string) *ModuleDiff {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, mod := range d.Modules {
|
||||||
|
if mod.Path == nil {
|
||||||
|
panic("missing module path")
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(mod.Path, path) {
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootModule returns the ModuleState for the root module
|
||||||
|
func (d *Diff) RootModule() *ModuleDiff {
|
||||||
|
root := d.ModuleByPath(rootModulePath)
|
||||||
|
if root == nil {
|
||||||
|
panic("missing root module")
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Diff) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, m := range d.Modules {
|
||||||
|
mStr := m.String()
|
||||||
|
|
||||||
|
// If we're the root module, we just write the output directly.
|
||||||
|
if reflect.DeepEqual(m.Path, rootModulePath) {
|
||||||
|
buf.WriteString(mStr + "\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(fmt.Sprintf("module.%s:\n", strings.Join(m.Path[1:], ".")))
|
||||||
|
|
||||||
|
s := bufio.NewScanner(strings.NewReader(mStr))
|
||||||
|
for s.Scan() {
|
||||||
|
buf.WriteString(fmt.Sprintf(" %s\n", s.Text()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Diff) init() {
|
func (d *Diff) init() {
|
||||||
d.once.Do(func() {
|
if d.Modules == nil {
|
||||||
|
rootDiff := &ModuleDiff{Path: rootModulePath}
|
||||||
|
d.Modules = []*ModuleDiff{rootDiff}
|
||||||
|
}
|
||||||
|
for _, m := range d.Modules {
|
||||||
|
m.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleDiff tracks the differences between resources to apply within
|
||||||
|
// a single module.
|
||||||
|
type ModuleDiff struct {
|
||||||
|
Path []string
|
||||||
|
Resources map[string]*InstanceDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ModuleDiff) init() {
|
||||||
if d.Resources == nil {
|
if d.Resources == nil {
|
||||||
d.Resources = make(map[string]*InstanceDiff)
|
d.Resources = make(map[string]*InstanceDiff)
|
||||||
}
|
}
|
||||||
})
|
for _, r := range d.Resources {
|
||||||
|
r.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty returns true if the diff has no changes.
|
// Empty returns true if the diff has no changes within this module.
|
||||||
func (d *Diff) Empty() bool {
|
func (d *ModuleDiff) Empty() bool {
|
||||||
if len(d.Resources) == 0 {
|
if len(d.Resources) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -39,7 +108,7 @@ func (d *Diff) Empty() bool {
|
||||||
|
|
||||||
// String outputs the diff in a long but command-line friendly output
|
// String outputs the diff in a long but command-line friendly output
|
||||||
// format that users can read to quickly inspect a diff.
|
// format that users can read to quickly inspect a diff.
|
||||||
func (d *Diff) String() string {
|
func (d *ModuleDiff) String() string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
names := make([]string, 0, len(d.Resources))
|
names := make([]string, 0, len(d.Resources))
|
||||||
|
@ -110,8 +179,6 @@ type InstanceDiff struct {
|
||||||
Attributes map[string]*ResourceAttrDiff
|
Attributes map[string]*ResourceAttrDiff
|
||||||
Destroy bool
|
Destroy bool
|
||||||
DestroyTainted bool
|
DestroyTainted bool
|
||||||
|
|
||||||
once sync.Once
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceAttrDiff is the diff of a single attribute of a resource.
|
// ResourceAttrDiff is the diff of a single attribute of a resource.
|
||||||
|
@ -143,11 +210,9 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *InstanceDiff) init() {
|
func (d *InstanceDiff) init() {
|
||||||
d.once.Do(func() {
|
|
||||||
if d.Attributes == nil {
|
if d.Attributes == nil {
|
||||||
d.Attributes = make(map[string]*ResourceAttrDiff)
|
d.Attributes = make(map[string]*ResourceAttrDiff)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty returns true if this diff encapsulates no changes.
|
// Empty returns true if this diff encapsulates no changes.
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiff_Empty(t *testing.T) {
|
func TestModuleDiff_Empty(t *testing.T) {
|
||||||
diff := new(Diff)
|
diff := new(ModuleDiff)
|
||||||
if !diff.Empty() {
|
if !diff.Empty() {
|
||||||
t.Fatal("should be empty")
|
t.Fatal("should be empty")
|
||||||
}
|
}
|
||||||
|
@ -38,8 +38,8 @@ func TestDiff_Empty(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiff_String(t *testing.T) {
|
func TestModuleDiff_String(t *testing.T) {
|
||||||
diff := &Diff{
|
diff := &ModuleDiff{
|
||||||
Resources: map[string]*InstanceDiff{
|
Resources: map[string]*InstanceDiff{
|
||||||
"nodeA": &InstanceDiff{
|
"nodeA": &InstanceDiff{
|
||||||
Attributes: map[string]*ResourceAttrDiff{
|
Attributes: map[string]*ResourceAttrDiff{
|
||||||
|
@ -62,7 +62,7 @@ func TestDiff_String(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := strings.TrimSpace(diff.String())
|
actual := strings.TrimSpace(diff.String())
|
||||||
expected := strings.TrimSpace(diffStrBasic)
|
expected := strings.TrimSpace(moduleDiffStrBasic)
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
t.Fatalf("bad:\n%s", actual)
|
t.Fatalf("bad:\n%s", actual)
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ func TestResourceDiffSame(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const diffStrBasic = `
|
const moduleDiffStrBasic = `
|
||||||
CREATE: nodeA
|
CREATE: nodeA
|
||||||
bar: "foo" => "<computed>"
|
bar: "foo" => "<computed>"
|
||||||
foo: "foo" => "bar"
|
foo: "foo" => "bar"
|
||||||
|
|
|
@ -132,10 +132,14 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
|
||||||
|
|
||||||
config := currentModule.Config()
|
config := currentModule.Config()
|
||||||
|
|
||||||
// Get the state of the module that we're working with.
|
// Get the state and diff of the module that we're working with.
|
||||||
var mod *ModuleState
|
var modDiff *ModuleDiff
|
||||||
|
var modState *ModuleState
|
||||||
|
if opts.Diff != nil {
|
||||||
|
modDiff = opts.Diff.ModuleByPath(opts.ModulePath)
|
||||||
|
}
|
||||||
if opts.State != nil {
|
if opts.State != nil {
|
||||||
mod = opts.State.ModuleByPath(opts.ModulePath)
|
modState = opts.State.ModuleByPath(opts.ModulePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Creating graph...")
|
log.Printf("[DEBUG] Creating graph...")
|
||||||
|
@ -145,7 +149,7 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
|
||||||
// First, build the initial resource graph. This only has the resources
|
// First, build the initial resource graph. This only has the resources
|
||||||
// and no dependencies. This only adds resources that are in the config
|
// and no dependencies. This only adds resources that are in the config
|
||||||
// and not "orphans" (that are in the state, but not in the config).
|
// and not "orphans" (that are in the state, but not in the config).
|
||||||
graphAddConfigResources(g, config, mod)
|
graphAddConfigResources(g, config, modState)
|
||||||
|
|
||||||
// Add the modules that are in the configuration.
|
// Add the modules that are in the configuration.
|
||||||
if err := graphAddConfigModules(g, config, opts); err != nil {
|
if err := graphAddConfigModules(g, config, opts); err != nil {
|
||||||
|
@ -155,12 +159,12 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
|
||||||
// Add explicit dependsOn dependencies to the graph
|
// Add explicit dependsOn dependencies to the graph
|
||||||
graphAddExplicitDeps(g)
|
graphAddExplicitDeps(g)
|
||||||
|
|
||||||
if mod != nil {
|
if modState != nil {
|
||||||
// Next, add the state orphans if we have any
|
// Next, add the state orphans if we have any
|
||||||
graphAddOrphans(g, config, mod)
|
graphAddOrphans(g, config, modState)
|
||||||
|
|
||||||
// Add tainted resources if we have any.
|
// Add tainted resources if we have any.
|
||||||
graphAddTainted(g, mod)
|
graphAddTainted(g, modState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map the provider configurations to all of the resources
|
// Map the provider configurations to all of the resources
|
||||||
|
@ -198,8 +202,8 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a diff, then make sure to add that in
|
// If we have a diff, then make sure to add that in
|
||||||
if opts.Diff != nil {
|
if modDiff != nil {
|
||||||
if err := graphAddDiff(g, opts.Diff); err != nil {
|
if err := graphAddDiff(g, modDiff); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -423,7 +427,7 @@ func graphAddConfigResources(
|
||||||
// destroying the VPC's subnets first, whereas creating a VPC requires
|
// destroying the VPC's subnets first, whereas creating a VPC requires
|
||||||
// doing it before the subnets are created. This function handles inserting
|
// doing it before the subnets are created. This function handles inserting
|
||||||
// these nodes for you.
|
// these nodes for you.
|
||||||
func graphAddDiff(g *depgraph.Graph, d *Diff) error {
|
func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
|
||||||
var nlist []*depgraph.Noun
|
var nlist []*depgraph.Noun
|
||||||
for _, n := range g.Nouns {
|
for _, n := range g.Nouns {
|
||||||
rn, ok := n.Meta.(*GraphNodeResource)
|
rn, ok := n.Meta.(*GraphNodeResource)
|
||||||
|
|
|
@ -357,6 +357,9 @@ func TestGraphProvisioners(t *testing.T) {
|
||||||
func TestGraphAddDiff(t *testing.T) {
|
func TestGraphAddDiff(t *testing.T) {
|
||||||
m := testModule(t, "graph-diff")
|
m := testModule(t, "graph-diff")
|
||||||
diff := &Diff{
|
diff := &Diff{
|
||||||
|
Modules: []*ModuleDiff{
|
||||||
|
&ModuleDiff{
|
||||||
|
Path: rootModulePath,
|
||||||
Resources: map[string]*InstanceDiff{
|
Resources: map[string]*InstanceDiff{
|
||||||
"aws_instance.foo": &InstanceDiff{
|
"aws_instance.foo": &InstanceDiff{
|
||||||
Attributes: map[string]*ResourceAttrDiff{
|
Attributes: map[string]*ResourceAttrDiff{
|
||||||
|
@ -366,6 +369,8 @@ func TestGraphAddDiff(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := Graph(&GraphOpts{Module: m, Diff: diff})
|
g, err := Graph(&GraphOpts{Module: m, Diff: diff})
|
||||||
|
@ -383,7 +388,7 @@ func TestGraphAddDiff(t *testing.T) {
|
||||||
n := g.Noun("aws_instance.foo")
|
n := g.Noun("aws_instance.foo")
|
||||||
rn := n.Meta.(*GraphNodeResource)
|
rn := n.Meta.(*GraphNodeResource)
|
||||||
|
|
||||||
expected2 := diff.Resources["aws_instance.foo"]
|
expected2 := diff.RootModule().Resources["aws_instance.foo"]
|
||||||
actual2 := rn.Resource.Diff
|
actual2 := rn.Resource.Diff
|
||||||
if !reflect.DeepEqual(actual2, expected2) {
|
if !reflect.DeepEqual(actual2, expected2) {
|
||||||
t.Fatalf("bad: %#v", actual2)
|
t.Fatalf("bad: %#v", actual2)
|
||||||
|
@ -393,6 +398,9 @@ func TestGraphAddDiff(t *testing.T) {
|
||||||
func TestGraphAddDiff_destroy(t *testing.T) {
|
func TestGraphAddDiff_destroy(t *testing.T) {
|
||||||
m := testModule(t, "graph-diff-destroy")
|
m := testModule(t, "graph-diff-destroy")
|
||||||
diff := &Diff{
|
diff := &Diff{
|
||||||
|
Modules: []*ModuleDiff{
|
||||||
|
&ModuleDiff{
|
||||||
|
Path: rootModulePath,
|
||||||
Resources: map[string]*InstanceDiff{
|
Resources: map[string]*InstanceDiff{
|
||||||
"aws_instance.foo": &InstanceDiff{
|
"aws_instance.foo": &InstanceDiff{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
|
@ -401,6 +409,8 @@ func TestGraphAddDiff_destroy(t *testing.T) {
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
state := &State{
|
state := &State{
|
||||||
Modules: []*ModuleState{
|
Modules: []*ModuleState{
|
||||||
|
@ -463,6 +473,9 @@ func TestGraphAddDiff_destroy(t *testing.T) {
|
||||||
func TestGraphAddDiff_destroy_counts(t *testing.T) {
|
func TestGraphAddDiff_destroy_counts(t *testing.T) {
|
||||||
m := testModule(t, "graph-count")
|
m := testModule(t, "graph-count")
|
||||||
diff := &Diff{
|
diff := &Diff{
|
||||||
|
Modules: []*ModuleDiff{
|
||||||
|
&ModuleDiff{
|
||||||
|
Path: rootModulePath,
|
||||||
Resources: map[string]*InstanceDiff{
|
Resources: map[string]*InstanceDiff{
|
||||||
"aws_instance.web.0": &InstanceDiff{
|
"aws_instance.web.0": &InstanceDiff{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
|
@ -477,6 +490,8 @@ func TestGraphAddDiff_destroy_counts(t *testing.T) {
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
state := &State{
|
state := &State{
|
||||||
Modules: []*ModuleState{
|
Modules: []*ModuleState{
|
||||||
|
|
|
@ -55,7 +55,7 @@ func (p *Plan) String() string {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
buf.WriteString("DIFF:\n\n")
|
buf.WriteString("DIFF:\n\n")
|
||||||
buf.WriteString(p.Diff.String())
|
buf.WriteString(p.Diff.String())
|
||||||
buf.WriteString("\nSTATE:\n\n")
|
buf.WriteString("\n\nSTATE:\n\n")
|
||||||
buf.WriteString(p.State.String())
|
buf.WriteString(p.State.String())
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ func TestReadWritePlan(t *testing.T) {
|
||||||
plan := &Plan{
|
plan := &Plan{
|
||||||
Config: testConfig(t, "new-good"),
|
Config: testConfig(t, "new-good"),
|
||||||
Diff: &Diff{
|
Diff: &Diff{
|
||||||
|
Modules: []*ModuleDiff{
|
||||||
|
&ModuleDiff{
|
||||||
|
Path: rootModulePath,
|
||||||
Resources: map[string]*InstanceDiff{
|
Resources: map[string]*InstanceDiff{
|
||||||
"nodeA": &InstanceDiff{
|
"nodeA": &InstanceDiff{
|
||||||
Attributes: map[string]*ResourceAttrDiff{
|
Attributes: map[string]*ResourceAttrDiff{
|
||||||
|
@ -31,6 +34,8 @@ func TestReadWritePlan(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
State: &State{
|
State: &State{
|
||||||
Modules: []*ModuleState{
|
Modules: []*ModuleState{
|
||||||
&ModuleState{
|
&ModuleState{
|
||||||
|
|
Loading…
Reference in New Issue