github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/daemon/api_interfaces_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package daemon_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "net/http" 27 "net/http/httptest" 28 "strings" 29 30 "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/client" 33 "github.com/snapcore/snapd/daemon" 34 "github.com/snapcore/snapd/interfaces" 35 "github.com/snapcore/snapd/interfaces/builtin" 36 "github.com/snapcore/snapd/interfaces/ifacetest" 37 "github.com/snapcore/snapd/overlord/ifacestate" 38 "github.com/snapcore/snapd/overlord/state" 39 ) 40 41 var _ = check.Suite(&interfacesSuite{}) 42 43 type interfacesSuite struct { 44 apiBaseSuite 45 } 46 47 func mockIface(c *check.C, d *daemon.Daemon, iface interfaces.Interface) { 48 err := d.Overlord().InterfaceManager().Repository().AddInterface(iface) 49 c.Assert(err, check.IsNil) 50 } 51 52 // inverseCaseMapper implements SnapMapper to use lower case internally and upper case externally. 53 type inverseCaseMapper struct { 54 ifacestate.IdentityMapper // Embed the identity mapper to reuse empty state mapping functions. 55 } 56 57 func (m *inverseCaseMapper) RemapSnapFromRequest(snapName string) string { 58 return strings.ToLower(snapName) 59 } 60 61 func (m *inverseCaseMapper) RemapSnapToResponse(snapName string) string { 62 return strings.ToUpper(snapName) 63 } 64 65 func (m *inverseCaseMapper) SystemSnapName() string { 66 return "core" 67 } 68 69 // Tests for POST /v2/interfaces 70 71 const ( 72 consumerYaml = ` 73 name: consumer 74 version: 1 75 apps: 76 app: 77 plugs: 78 plug: 79 interface: test 80 key: value 81 label: label 82 ` 83 84 producerYaml = ` 85 name: producer 86 version: 1 87 apps: 88 app: 89 slots: 90 slot: 91 interface: test 92 key: value 93 label: label 94 ` 95 96 coreProducerYaml = ` 97 name: core 98 version: 1 99 slots: 100 slot: 101 interface: test 102 key: value 103 label: label 104 ` 105 106 differentProducerYaml = ` 107 name: producer 108 version: 1 109 apps: 110 app: 111 slots: 112 slot: 113 interface: different 114 key: value 115 label: label 116 ` 117 ) 118 119 func (s *interfacesSuite) TestConnectPlugSuccess(c *check.C) { 120 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 121 defer restore() 122 // Install an inverse case mapper to exercise the interface mapping at the same time. 123 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 124 defer restore() 125 126 d := s.daemon(c) 127 128 s.mockSnap(c, consumerYaml) 129 s.mockSnap(c, producerYaml) 130 131 d.Overlord().Loop() 132 defer d.Overlord().Stop() 133 134 action := &client.InterfaceAction{ 135 Action: "connect", 136 Plugs: []client.Plug{{Snap: "CONSUMER", Name: "plug"}}, 137 Slots: []client.Slot{{Snap: "PRODUCER", Name: "slot"}}, 138 } 139 text, err := json.Marshal(action) 140 c.Assert(err, check.IsNil) 141 buf := bytes.NewBuffer(text) 142 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 143 c.Assert(err, check.IsNil) 144 rec := httptest.NewRecorder() 145 s.req(c, req, nil).ServeHTTP(rec, req) 146 c.Check(rec.Code, check.Equals, 202) 147 var body map[string]interface{} 148 err = json.Unmarshal(rec.Body.Bytes(), &body) 149 c.Check(err, check.IsNil) 150 id := body["change"].(string) 151 152 st := d.Overlord().State() 153 st.Lock() 154 chg := st.Change(id) 155 st.Unlock() 156 c.Assert(chg, check.NotNil) 157 158 <-chg.Ready() 159 160 st.Lock() 161 err = chg.Err() 162 st.Unlock() 163 c.Assert(err, check.IsNil) 164 165 repo := d.Overlord().InterfaceManager().Repository() 166 ifaces := repo.Interfaces() 167 c.Assert(ifaces.Connections, check.HasLen, 1) 168 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 169 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 170 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 171 }}) 172 } 173 174 func (s *interfacesSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) { 175 d := s.daemon(c) 176 177 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 178 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "different"}) 179 s.mockSnap(c, consumerYaml) 180 s.mockSnap(c, differentProducerYaml) 181 182 action := &client.InterfaceAction{ 183 Action: "connect", 184 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 185 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 186 } 187 text, err := json.Marshal(action) 188 c.Assert(err, check.IsNil) 189 buf := bytes.NewBuffer(text) 190 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 191 c.Assert(err, check.IsNil) 192 rec := httptest.NewRecorder() 193 s.req(c, req, nil).ServeHTTP(rec, req) 194 c.Check(rec.Code, check.Equals, 400) 195 var body map[string]interface{} 196 err = json.Unmarshal(rec.Body.Bytes(), &body) 197 c.Check(err, check.IsNil) 198 c.Check(body, check.DeepEquals, map[string]interface{}{ 199 "result": map[string]interface{}{ 200 "message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)", 201 }, 202 "status": "Bad Request", 203 "status-code": 400.0, 204 "type": "error", 205 }) 206 repo := d.Overlord().InterfaceManager().Repository() 207 ifaces := repo.Interfaces() 208 c.Assert(ifaces.Connections, check.HasLen, 0) 209 } 210 211 func (s *interfacesSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) { 212 d := s.daemon(c) 213 214 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 215 // there is no consumer, no plug defined 216 s.mockSnap(c, producerYaml) 217 s.mockSnap(c, consumerYaml) 218 219 action := &client.InterfaceAction{ 220 Action: "connect", 221 Plugs: []client.Plug{{Snap: "consumer", Name: "missingplug"}}, 222 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 223 } 224 text, err := json.Marshal(action) 225 c.Assert(err, check.IsNil) 226 buf := bytes.NewBuffer(text) 227 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 228 c.Assert(err, check.IsNil) 229 rec := httptest.NewRecorder() 230 s.req(c, req, nil).ServeHTTP(rec, req) 231 c.Check(rec.Code, check.Equals, 400) 232 233 var body map[string]interface{} 234 err = json.Unmarshal(rec.Body.Bytes(), &body) 235 c.Check(err, check.IsNil) 236 c.Check(body, check.DeepEquals, map[string]interface{}{ 237 "result": map[string]interface{}{ 238 "message": "snap \"consumer\" has no plug named \"missingplug\"", 239 }, 240 "status": "Bad Request", 241 "status-code": 400.0, 242 "type": "error", 243 }) 244 245 repo := d.Overlord().InterfaceManager().Repository() 246 ifaces := repo.Interfaces() 247 c.Assert(ifaces.Connections, check.HasLen, 0) 248 } 249 250 func (s *interfacesSuite) TestConnectAlreadyConnected(c *check.C) { 251 d := s.daemon(c) 252 253 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 254 // there is no consumer, no plug defined 255 s.mockSnap(c, producerYaml) 256 s.mockSnap(c, consumerYaml) 257 258 repo := d.Overlord().InterfaceManager().Repository() 259 connRef := &interfaces.ConnRef{ 260 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 261 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 262 } 263 264 d.Overlord().Loop() 265 defer d.Overlord().Stop() 266 267 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 268 c.Assert(err, check.IsNil) 269 conns := map[string]interface{}{ 270 "consumer:plug producer:slot": map[string]interface{}{ 271 "auto": false, 272 }, 273 } 274 st := d.Overlord().State() 275 st.Lock() 276 st.Set("conns", conns) 277 st.Unlock() 278 279 action := &client.InterfaceAction{ 280 Action: "connect", 281 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 282 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 283 } 284 text, err := json.Marshal(action) 285 c.Assert(err, check.IsNil) 286 buf := bytes.NewBuffer(text) 287 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 288 c.Assert(err, check.IsNil) 289 rec := httptest.NewRecorder() 290 s.req(c, req, nil).ServeHTTP(rec, req) 291 c.Check(rec.Code, check.Equals, 202) 292 var body map[string]interface{} 293 err = json.Unmarshal(rec.Body.Bytes(), &body) 294 c.Check(err, check.IsNil) 295 id := body["change"].(string) 296 297 st.Lock() 298 chg := st.Change(id) 299 c.Assert(chg.Tasks(), check.HasLen, 0) 300 c.Assert(chg.Status(), check.Equals, state.DoneStatus) 301 st.Unlock() 302 } 303 304 func (s *interfacesSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) { 305 d := s.daemon(c) 306 307 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 308 s.mockSnap(c, consumerYaml) 309 s.mockSnap(c, producerYaml) 310 // there is no producer, no slot defined 311 312 action := &client.InterfaceAction{ 313 Action: "connect", 314 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 315 Slots: []client.Slot{{Snap: "producer", Name: "missingslot"}}, 316 } 317 text, err := json.Marshal(action) 318 c.Assert(err, check.IsNil) 319 buf := bytes.NewBuffer(text) 320 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 321 c.Assert(err, check.IsNil) 322 rec := httptest.NewRecorder() 323 s.req(c, req, nil).ServeHTTP(rec, req) 324 c.Check(rec.Code, check.Equals, 400) 325 326 var body map[string]interface{} 327 err = json.Unmarshal(rec.Body.Bytes(), &body) 328 c.Check(err, check.IsNil) 329 c.Check(body, check.DeepEquals, map[string]interface{}{ 330 "result": map[string]interface{}{ 331 "message": "snap \"producer\" has no slot named \"missingslot\"", 332 }, 333 "status": "Bad Request", 334 "status-code": 400.0, 335 "type": "error", 336 }) 337 338 repo := d.Overlord().InterfaceManager().Repository() 339 ifaces := repo.Interfaces() 340 c.Assert(ifaces.Connections, check.HasLen, 0) 341 } 342 343 func (s *interfacesSuite) testConnectFailureNoSnap(c *check.C, installedSnap string) { 344 // sanity, either consumer or producer needs to be enabled 345 consumer := installedSnap == "consumer" 346 producer := installedSnap == "producer" 347 c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer")) 348 349 d := s.daemon(c) 350 351 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 352 353 if consumer { 354 s.mockSnap(c, consumerYaml) 355 } 356 if producer { 357 s.mockSnap(c, producerYaml) 358 } 359 360 action := &client.InterfaceAction{ 361 Action: "connect", 362 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 363 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 364 } 365 text, err := json.Marshal(action) 366 c.Assert(err, check.IsNil) 367 buf := bytes.NewBuffer(text) 368 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 369 c.Assert(err, check.IsNil) 370 rec := httptest.NewRecorder() 371 s.req(c, req, nil).ServeHTTP(rec, req) 372 c.Check(rec.Code, check.Equals, 400) 373 374 var body map[string]interface{} 375 err = json.Unmarshal(rec.Body.Bytes(), &body) 376 c.Check(err, check.IsNil) 377 if producer { 378 c.Check(body, check.DeepEquals, map[string]interface{}{ 379 "result": map[string]interface{}{ 380 "message": "snap \"consumer\" is not installed", 381 }, 382 "status": "Bad Request", 383 "status-code": 400.0, 384 "type": "error", 385 }) 386 } else { 387 c.Check(body, check.DeepEquals, map[string]interface{}{ 388 "result": map[string]interface{}{ 389 "message": "snap \"producer\" is not installed", 390 }, 391 "status": "Bad Request", 392 "status-code": 400.0, 393 "type": "error", 394 }) 395 } 396 } 397 398 func (s *interfacesSuite) TestConnectPlugFailureNoPlugSnap(c *check.C) { 399 s.testConnectFailureNoSnap(c, "producer") 400 } 401 402 func (s *interfacesSuite) TestConnectPlugFailureNoSlotSnap(c *check.C) { 403 s.testConnectFailureNoSnap(c, "consumer") 404 } 405 406 func (s *interfacesSuite) TestConnectPlugChangeConflict(c *check.C) { 407 d := s.daemon(c) 408 409 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 410 s.mockSnap(c, consumerYaml) 411 s.mockSnap(c, producerYaml) 412 // there is no producer, no slot defined 413 414 s.simulateConflict("consumer") 415 416 action := &client.InterfaceAction{ 417 Action: "connect", 418 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 419 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 420 } 421 text, err := json.Marshal(action) 422 c.Assert(err, check.IsNil) 423 buf := bytes.NewBuffer(text) 424 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 425 c.Assert(err, check.IsNil) 426 rec := httptest.NewRecorder() 427 s.req(c, req, nil).ServeHTTP(rec, req) 428 c.Check(rec.Code, check.Equals, 409) 429 430 var body map[string]interface{} 431 err = json.Unmarshal(rec.Body.Bytes(), &body) 432 c.Check(err, check.IsNil) 433 c.Check(body, check.DeepEquals, map[string]interface{}{ 434 "status-code": 409., 435 "status": "Conflict", 436 "result": map[string]interface{}{ 437 "message": `snap "consumer" has "manip" change in progress`, 438 "kind": "snap-change-conflict", 439 "value": map[string]interface{}{ 440 "change-kind": "manip", 441 "snap-name": "consumer", 442 }, 443 }, 444 "type": "error"}) 445 } 446 447 func (s *interfacesSuite) TestConnectCoreSystemAlias(c *check.C) { 448 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 449 defer revert() 450 d := s.daemon(c) 451 452 s.mockSnap(c, consumerYaml) 453 s.mockSnap(c, coreProducerYaml) 454 455 d.Overlord().Loop() 456 defer d.Overlord().Stop() 457 458 action := &client.InterfaceAction{ 459 Action: "connect", 460 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 461 Slots: []client.Slot{{Snap: "system", Name: "slot"}}, 462 } 463 text, err := json.Marshal(action) 464 c.Assert(err, check.IsNil) 465 buf := bytes.NewBuffer(text) 466 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 467 c.Assert(err, check.IsNil) 468 rec := httptest.NewRecorder() 469 s.req(c, req, nil).ServeHTTP(rec, req) 470 c.Check(rec.Code, check.Equals, 202) 471 var body map[string]interface{} 472 err = json.Unmarshal(rec.Body.Bytes(), &body) 473 c.Check(err, check.IsNil) 474 id := body["change"].(string) 475 476 st := d.Overlord().State() 477 st.Lock() 478 chg := st.Change(id) 479 st.Unlock() 480 c.Assert(chg, check.NotNil) 481 482 <-chg.Ready() 483 484 st.Lock() 485 err = chg.Err() 486 st.Unlock() 487 c.Assert(err, check.IsNil) 488 489 repo := d.Overlord().InterfaceManager().Repository() 490 ifaces := repo.Interfaces() 491 c.Assert(ifaces.Connections, check.HasLen, 1) 492 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 493 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 494 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}}}) 495 } 496 497 func (s *interfacesSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) { 498 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 499 defer restore() 500 // Install an inverse case mapper to exercise the interface mapping at the same time. 501 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 502 defer restore() 503 d := s.daemon(c) 504 505 s.mockSnap(c, consumerYaml) 506 s.mockSnap(c, producerYaml) 507 508 repo := d.Overlord().InterfaceManager().Repository() 509 connRef := &interfaces.ConnRef{ 510 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 511 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 512 } 513 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 514 c.Assert(err, check.IsNil) 515 516 st := d.Overlord().State() 517 st.Lock() 518 st.Set("conns", map[string]interface{}{ 519 "consumer:plug producer:slot": map[string]interface{}{ 520 "interface": "test", 521 }, 522 }) 523 st.Unlock() 524 525 d.Overlord().Loop() 526 defer d.Overlord().Stop() 527 528 action := &client.InterfaceAction{ 529 Action: "disconnect", 530 Plugs: []client.Plug{{Snap: plugSnap, Name: plugName}}, 531 Slots: []client.Slot{{Snap: slotSnap, Name: slotName}}, 532 } 533 text, err := json.Marshal(action) 534 c.Assert(err, check.IsNil) 535 buf := bytes.NewBuffer(text) 536 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 537 c.Assert(err, check.IsNil) 538 rec := httptest.NewRecorder() 539 s.req(c, req, nil).ServeHTTP(rec, req) 540 c.Check(rec.Code, check.Equals, 202) 541 var body map[string]interface{} 542 err = json.Unmarshal(rec.Body.Bytes(), &body) 543 c.Check(err, check.IsNil) 544 id := body["change"].(string) 545 546 st.Lock() 547 chg := st.Change(id) 548 st.Unlock() 549 c.Assert(chg, check.NotNil) 550 551 <-chg.Ready() 552 553 st.Lock() 554 err = chg.Err() 555 st.Unlock() 556 c.Assert(err, check.IsNil) 557 558 ifaces := repo.Interfaces() 559 c.Assert(ifaces.Connections, check.HasLen, 0) 560 } 561 562 func (s *interfacesSuite) TestDisconnectPlugSuccess(c *check.C) { 563 s.testDisconnect(c, "CONSUMER", "plug", "PRODUCER", "slot") 564 } 565 566 func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) { 567 s.testDisconnect(c, "", "", "PRODUCER", "slot") 568 } 569 570 func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) { 571 s.testDisconnect(c, "CONSUMER", "plug", "", "") 572 } 573 574 func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) { 575 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 576 defer revert() 577 s.daemon(c) 578 579 s.mockSnap(c, consumerYaml) 580 s.mockSnap(c, producerYaml) 581 582 action := &client.InterfaceAction{ 583 Action: "disconnect", 584 Plugs: []client.Plug{{Snap: "consumer", Name: "missingplug"}}, 585 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 586 } 587 text, err := json.Marshal(action) 588 c.Assert(err, check.IsNil) 589 buf := bytes.NewBuffer(text) 590 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 591 c.Assert(err, check.IsNil) 592 rec := httptest.NewRecorder() 593 s.req(c, req, nil).ServeHTTP(rec, req) 594 c.Check(rec.Code, check.Equals, 400) 595 var body map[string]interface{} 596 err = json.Unmarshal(rec.Body.Bytes(), &body) 597 c.Check(err, check.IsNil) 598 c.Check(body, check.DeepEquals, map[string]interface{}{ 599 "result": map[string]interface{}{ 600 "message": "snap \"consumer\" has no plug named \"missingplug\"", 601 }, 602 "status": "Bad Request", 603 "status-code": 400.0, 604 "type": "error", 605 }) 606 } 607 608 func (s *interfacesSuite) testDisconnectFailureNoSnap(c *check.C, installedSnap string) { 609 // sanity, either consumer or producer needs to be enabled 610 consumer := installedSnap == "consumer" 611 producer := installedSnap == "producer" 612 c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer")) 613 614 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 615 defer revert() 616 s.daemon(c) 617 618 if consumer { 619 s.mockSnap(c, consumerYaml) 620 } 621 if producer { 622 s.mockSnap(c, producerYaml) 623 } 624 625 action := &client.InterfaceAction{ 626 Action: "disconnect", 627 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 628 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 629 } 630 text, err := json.Marshal(action) 631 c.Assert(err, check.IsNil) 632 buf := bytes.NewBuffer(text) 633 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 634 c.Assert(err, check.IsNil) 635 rec := httptest.NewRecorder() 636 s.req(c, req, nil).ServeHTTP(rec, req) 637 c.Check(rec.Code, check.Equals, 400) 638 var body map[string]interface{} 639 err = json.Unmarshal(rec.Body.Bytes(), &body) 640 c.Check(err, check.IsNil) 641 642 if producer { 643 c.Check(body, check.DeepEquals, map[string]interface{}{ 644 "result": map[string]interface{}{ 645 "message": "snap \"consumer\" is not installed", 646 }, 647 "status": "Bad Request", 648 "status-code": 400.0, 649 "type": "error", 650 }) 651 } else { 652 c.Check(body, check.DeepEquals, map[string]interface{}{ 653 "result": map[string]interface{}{ 654 "message": "snap \"producer\" is not installed", 655 }, 656 "status": "Bad Request", 657 "status-code": 400.0, 658 "type": "error", 659 }) 660 } 661 } 662 663 func (s *interfacesSuite) TestDisconnectPlugFailureNoPlugSnap(c *check.C) { 664 s.testDisconnectFailureNoSnap(c, "producer") 665 } 666 667 func (s *interfacesSuite) TestDisconnectPlugFailureNoSlotSnap(c *check.C) { 668 s.testDisconnectFailureNoSnap(c, "consumer") 669 } 670 671 func (s *interfacesSuite) TestDisconnectPlugNothingToDo(c *check.C) { 672 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 673 defer revert() 674 s.daemon(c) 675 676 s.mockSnap(c, consumerYaml) 677 s.mockSnap(c, producerYaml) 678 679 action := &client.InterfaceAction{ 680 Action: "disconnect", 681 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 682 Slots: []client.Slot{{Snap: "", Name: ""}}, 683 } 684 text, err := json.Marshal(action) 685 c.Assert(err, check.IsNil) 686 buf := bytes.NewBuffer(text) 687 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 688 c.Assert(err, check.IsNil) 689 rec := httptest.NewRecorder() 690 s.req(c, req, nil).ServeHTTP(rec, req) 691 c.Check(rec.Code, check.Equals, 400) 692 var body map[string]interface{} 693 err = json.Unmarshal(rec.Body.Bytes(), &body) 694 c.Check(err, check.IsNil) 695 c.Check(body, check.DeepEquals, map[string]interface{}{ 696 "result": map[string]interface{}{ 697 "message": "nothing to do", 698 "kind": "interfaces-unchanged", 699 }, 700 "status": "Bad Request", 701 "status-code": 400.0, 702 "type": "error", 703 }) 704 } 705 706 func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) { 707 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 708 defer revert() 709 s.daemon(c) 710 711 s.mockSnap(c, consumerYaml) 712 s.mockSnap(c, producerYaml) 713 714 action := &client.InterfaceAction{ 715 Action: "disconnect", 716 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 717 Slots: []client.Slot{{Snap: "producer", Name: "missingslot"}}, 718 } 719 text, err := json.Marshal(action) 720 c.Assert(err, check.IsNil) 721 buf := bytes.NewBuffer(text) 722 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 723 c.Assert(err, check.IsNil) 724 rec := httptest.NewRecorder() 725 s.req(c, req, nil).ServeHTTP(rec, req) 726 727 c.Check(rec.Code, check.Equals, 400) 728 var body map[string]interface{} 729 err = json.Unmarshal(rec.Body.Bytes(), &body) 730 c.Check(err, check.IsNil) 731 c.Check(body, check.DeepEquals, map[string]interface{}{ 732 "result": map[string]interface{}{ 733 "message": "snap \"producer\" has no slot named \"missingslot\"", 734 }, 735 "status": "Bad Request", 736 "status-code": 400.0, 737 "type": "error", 738 }) 739 } 740 741 func (s *interfacesSuite) TestDisconnectPlugFailureNotConnected(c *check.C) { 742 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 743 defer revert() 744 s.daemon(c) 745 746 s.mockSnap(c, consumerYaml) 747 s.mockSnap(c, producerYaml) 748 749 action := &client.InterfaceAction{ 750 Action: "disconnect", 751 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 752 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 753 } 754 text, err := json.Marshal(action) 755 c.Assert(err, check.IsNil) 756 buf := bytes.NewBuffer(text) 757 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 758 c.Assert(err, check.IsNil) 759 rec := httptest.NewRecorder() 760 s.req(c, req, nil).ServeHTTP(rec, req) 761 762 c.Check(rec.Code, check.Equals, 400) 763 var body map[string]interface{} 764 err = json.Unmarshal(rec.Body.Bytes(), &body) 765 c.Check(err, check.IsNil) 766 c.Check(body, check.DeepEquals, map[string]interface{}{ 767 "result": map[string]interface{}{ 768 "message": "cannot disconnect consumer:plug from producer:slot, it is not connected", 769 }, 770 "status": "Bad Request", 771 "status-code": 400.0, 772 "type": "error", 773 }) 774 } 775 776 func (s *interfacesSuite) TestDisconnectForgetPlugFailureNotConnected(c *check.C) { 777 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 778 defer revert() 779 s.daemon(c) 780 781 s.mockSnap(c, consumerYaml) 782 s.mockSnap(c, producerYaml) 783 784 action := &client.InterfaceAction{ 785 Action: "disconnect", 786 Forget: true, 787 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 788 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 789 } 790 text, err := json.Marshal(action) 791 c.Assert(err, check.IsNil) 792 buf := bytes.NewBuffer(text) 793 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 794 c.Assert(err, check.IsNil) 795 rec := httptest.NewRecorder() 796 s.req(c, req, nil).ServeHTTP(rec, req) 797 798 c.Check(rec.Code, check.Equals, 400) 799 var body map[string]interface{} 800 err = json.Unmarshal(rec.Body.Bytes(), &body) 801 c.Check(err, check.IsNil) 802 c.Check(body, check.DeepEquals, map[string]interface{}{ 803 "result": map[string]interface{}{ 804 "message": "cannot forget connection consumer:plug from producer:slot, it was not connected", 805 }, 806 "status": "Bad Request", 807 "status-code": 400.0, 808 "type": "error", 809 }) 810 } 811 812 func (s *interfacesSuite) TestDisconnectConflict(c *check.C) { 813 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 814 defer revert() 815 d := s.daemon(c) 816 817 s.mockSnap(c, consumerYaml) 818 s.mockSnap(c, producerYaml) 819 820 repo := d.Overlord().InterfaceManager().Repository() 821 connRef := &interfaces.ConnRef{ 822 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 823 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 824 } 825 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 826 c.Assert(err, check.IsNil) 827 828 st := d.Overlord().State() 829 st.Lock() 830 st.Set("conns", map[string]interface{}{ 831 "consumer:plug producer:slot": map[string]interface{}{ 832 "interface": "test", 833 }, 834 }) 835 st.Unlock() 836 837 s.simulateConflict("consumer") 838 839 action := &client.InterfaceAction{ 840 Action: "disconnect", 841 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 842 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 843 } 844 text, err := json.Marshal(action) 845 c.Assert(err, check.IsNil) 846 buf := bytes.NewBuffer(text) 847 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 848 c.Assert(err, check.IsNil) 849 rec := httptest.NewRecorder() 850 s.req(c, req, nil).ServeHTTP(rec, req) 851 852 c.Check(rec.Code, check.Equals, 409) 853 854 var body map[string]interface{} 855 err = json.Unmarshal(rec.Body.Bytes(), &body) 856 c.Check(err, check.IsNil) 857 c.Check(body, check.DeepEquals, map[string]interface{}{ 858 "status-code": 409., 859 "status": "Conflict", 860 "result": map[string]interface{}{ 861 "message": `snap "consumer" has "manip" change in progress`, 862 "kind": "snap-change-conflict", 863 "value": map[string]interface{}{ 864 "change-kind": "manip", 865 "snap-name": "consumer", 866 }, 867 }, 868 "type": "error"}) 869 } 870 871 func (s *interfacesSuite) TestDisconnectCoreSystemAlias(c *check.C) { 872 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 873 defer revert() 874 d := s.daemon(c) 875 876 s.mockSnap(c, consumerYaml) 877 s.mockSnap(c, coreProducerYaml) 878 879 repo := d.Overlord().InterfaceManager().Repository() 880 connRef := &interfaces.ConnRef{ 881 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 882 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}, 883 } 884 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 885 c.Assert(err, check.IsNil) 886 887 st := d.Overlord().State() 888 st.Lock() 889 st.Set("conns", map[string]interface{}{ 890 "consumer:plug core:slot": map[string]interface{}{ 891 "interface": "test", 892 }, 893 }) 894 st.Unlock() 895 896 d.Overlord().Loop() 897 defer d.Overlord().Stop() 898 899 action := &client.InterfaceAction{ 900 Action: "disconnect", 901 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 902 Slots: []client.Slot{{Snap: "system", Name: "slot"}}, 903 } 904 text, err := json.Marshal(action) 905 c.Assert(err, check.IsNil) 906 buf := bytes.NewBuffer(text) 907 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 908 c.Assert(err, check.IsNil) 909 rec := httptest.NewRecorder() 910 s.req(c, req, nil).ServeHTTP(rec, req) 911 c.Check(rec.Code, check.Equals, 202) 912 var body map[string]interface{} 913 err = json.Unmarshal(rec.Body.Bytes(), &body) 914 c.Check(err, check.IsNil) 915 id := body["change"].(string) 916 917 st.Lock() 918 chg := st.Change(id) 919 st.Unlock() 920 c.Assert(chg, check.NotNil) 921 922 <-chg.Ready() 923 924 st.Lock() 925 err = chg.Err() 926 st.Unlock() 927 c.Assert(err, check.IsNil) 928 929 ifaces := repo.Interfaces() 930 c.Assert(ifaces.Connections, check.HasLen, 0) 931 } 932 933 func (s *interfacesSuite) TestUnsupportedInterfaceRequest(c *check.C) { 934 s.daemon(c) 935 buf := bytes.NewBuffer([]byte(`garbage`)) 936 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 937 c.Assert(err, check.IsNil) 938 rec := httptest.NewRecorder() 939 s.req(c, req, nil).ServeHTTP(rec, req) 940 c.Check(rec.Code, check.Equals, 400) 941 var body map[string]interface{} 942 err = json.Unmarshal(rec.Body.Bytes(), &body) 943 c.Check(err, check.IsNil) 944 c.Check(body, check.DeepEquals, map[string]interface{}{ 945 "result": map[string]interface{}{ 946 "message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value", 947 }, 948 "status": "Bad Request", 949 "status-code": 400.0, 950 "type": "error", 951 }) 952 } 953 954 func (s *interfacesSuite) TestMissingInterfaceAction(c *check.C) { 955 s.daemon(c) 956 action := &client.InterfaceAction{} 957 text, err := json.Marshal(action) 958 c.Assert(err, check.IsNil) 959 buf := bytes.NewBuffer(text) 960 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 961 c.Assert(err, check.IsNil) 962 rec := httptest.NewRecorder() 963 s.req(c, req, nil).ServeHTTP(rec, req) 964 c.Check(rec.Code, check.Equals, 400) 965 var body map[string]interface{} 966 err = json.Unmarshal(rec.Body.Bytes(), &body) 967 c.Check(err, check.IsNil) 968 c.Check(body, check.DeepEquals, map[string]interface{}{ 969 "result": map[string]interface{}{ 970 "message": "interface action not specified", 971 }, 972 "status": "Bad Request", 973 "status-code": 400.0, 974 "type": "error", 975 }) 976 } 977 978 func (s *interfacesSuite) TestUnsupportedInterfaceAction(c *check.C) { 979 s.daemon(c) 980 action := &client.InterfaceAction{Action: "foo"} 981 text, err := json.Marshal(action) 982 c.Assert(err, check.IsNil) 983 buf := bytes.NewBuffer(text) 984 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 985 c.Assert(err, check.IsNil) 986 rec := httptest.NewRecorder() 987 s.req(c, req, nil).ServeHTTP(rec, req) 988 c.Check(rec.Code, check.Equals, 400) 989 var body map[string]interface{} 990 err = json.Unmarshal(rec.Body.Bytes(), &body) 991 c.Check(err, check.IsNil) 992 c.Check(body, check.DeepEquals, map[string]interface{}{ 993 "result": map[string]interface{}{ 994 "message": "unsupported interface action: \"foo\"", 995 }, 996 "status": "Bad Request", 997 "status-code": 400.0, 998 "type": "error", 999 }) 1000 } 1001 1002 // Tests for GET /v2/interfaces 1003 1004 func (s *interfacesSuite) TestInterfacesLegacy(c *check.C) { 1005 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 1006 defer restore() 1007 // Install an inverse case mapper to exercise the interface mapping at the same time. 1008 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 1009 defer restore() 1010 1011 d := s.daemon(c) 1012 1013 var anotherConsumerYaml = ` 1014 name: another-consumer-%s 1015 version: 1 1016 apps: 1017 app: 1018 plugs: 1019 plug: 1020 interface: test 1021 key: value 1022 label: label 1023 ` 1024 s.mockSnap(c, consumerYaml) 1025 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def")) 1026 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc")) 1027 s.mockSnap(c, producerYaml) 1028 1029 repo := d.Overlord().InterfaceManager().Repository() 1030 connRef := &interfaces.ConnRef{ 1031 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 1032 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 1033 } 1034 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 1035 c.Assert(err, check.IsNil) 1036 1037 st := d.Overlord().State() 1038 st.Lock() 1039 st.Set("conns", map[string]interface{}{ 1040 "consumer:plug producer:slot": map[string]interface{}{ 1041 "interface": "test", 1042 "auto": true, 1043 }, 1044 "another-consumer-def:plug producer:slot": map[string]interface{}{ 1045 "interface": "test", 1046 "by-gadget": true, 1047 "auto": true, 1048 }, 1049 "another-consumer-abc:plug producer:slot": map[string]interface{}{ 1050 "interface": "test", 1051 "by-gadget": true, 1052 "auto": true, 1053 }, 1054 }) 1055 st.Unlock() 1056 1057 req, err := http.NewRequest("GET", "/v2/interfaces", nil) 1058 c.Assert(err, check.IsNil) 1059 rec := httptest.NewRecorder() 1060 s.req(c, req, nil).ServeHTTP(rec, req) 1061 c.Check(rec.Code, check.Equals, 200) 1062 var body map[string]interface{} 1063 err = json.Unmarshal(rec.Body.Bytes(), &body) 1064 c.Check(err, check.IsNil) 1065 c.Check(body, check.DeepEquals, map[string]interface{}{ 1066 "result": map[string]interface{}{ 1067 "plugs": []interface{}{ 1068 map[string]interface{}{ 1069 "snap": "another-consumer-abc", 1070 "plug": "plug", 1071 "interface": "test", 1072 "attrs": map[string]interface{}{"key": "value"}, 1073 "apps": []interface{}{"app"}, 1074 "label": "label", 1075 "connections": []interface{}{ 1076 map[string]interface{}{"snap": "producer", "slot": "slot"}, 1077 }, 1078 }, 1079 map[string]interface{}{ 1080 "snap": "another-consumer-def", 1081 "plug": "plug", 1082 "interface": "test", 1083 "attrs": map[string]interface{}{"key": "value"}, 1084 "apps": []interface{}{"app"}, 1085 "label": "label", 1086 "connections": []interface{}{ 1087 map[string]interface{}{"snap": "producer", "slot": "slot"}, 1088 }, 1089 }, 1090 map[string]interface{}{ 1091 "snap": "consumer", 1092 "plug": "plug", 1093 "interface": "test", 1094 "attrs": map[string]interface{}{"key": "value"}, 1095 "apps": []interface{}{"app"}, 1096 "label": "label", 1097 "connections": []interface{}{ 1098 map[string]interface{}{"snap": "producer", "slot": "slot"}, 1099 }, 1100 }, 1101 }, 1102 "slots": []interface{}{ 1103 map[string]interface{}{ 1104 "snap": "producer", 1105 "slot": "slot", 1106 "interface": "test", 1107 "attrs": map[string]interface{}{"key": "value"}, 1108 "apps": []interface{}{"app"}, 1109 "label": "label", 1110 "connections": []interface{}{ 1111 map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"}, 1112 map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"}, 1113 map[string]interface{}{"snap": "consumer", "plug": "plug"}, 1114 }, 1115 }, 1116 }, 1117 }, 1118 "status": "OK", 1119 "status-code": 200.0, 1120 "type": "sync", 1121 }) 1122 } 1123 1124 func (s *interfacesSuite) TestInterfacesModern(c *check.C) { 1125 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 1126 defer restore() 1127 // Install an inverse case mapper to exercise the interface mapping at the same time. 1128 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 1129 defer restore() 1130 1131 d := s.daemon(c) 1132 1133 s.mockSnap(c, consumerYaml) 1134 s.mockSnap(c, producerYaml) 1135 1136 repo := d.Overlord().InterfaceManager().Repository() 1137 connRef := &interfaces.ConnRef{ 1138 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 1139 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 1140 } 1141 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 1142 c.Assert(err, check.IsNil) 1143 1144 req, err := http.NewRequest("GET", "/v2/interfaces?select=connected&doc=true&plugs=true&slots=true", nil) 1145 c.Assert(err, check.IsNil) 1146 rec := httptest.NewRecorder() 1147 s.req(c, req, nil).ServeHTTP(rec, req) 1148 c.Check(rec.Code, check.Equals, 200) 1149 var body map[string]interface{} 1150 err = json.Unmarshal(rec.Body.Bytes(), &body) 1151 c.Check(err, check.IsNil) 1152 c.Check(body, check.DeepEquals, map[string]interface{}{ 1153 "result": []interface{}{ 1154 map[string]interface{}{ 1155 "name": "test", 1156 "plugs": []interface{}{ 1157 map[string]interface{}{ 1158 "snap": "consumer", 1159 "plug": "plug", 1160 "label": "label", 1161 "attrs": map[string]interface{}{ 1162 "key": "value", 1163 }, 1164 }}, 1165 "slots": []interface{}{ 1166 map[string]interface{}{ 1167 "snap": "producer", 1168 "slot": "slot", 1169 "label": "label", 1170 "attrs": map[string]interface{}{ 1171 "key": "value", 1172 }, 1173 }, 1174 }, 1175 }, 1176 }, 1177 "status": "OK", 1178 "status-code": 200.0, 1179 "type": "sync", 1180 }) 1181 }